Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

Feature: `icp cycles buy` — buy cycles with a card for the current identity via an on-chain gateway

Aperta
#812 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@raymondk ci sta già lavorando.

Dal 7/10/2026.

  • #813 di @raymondk — aperta
  • #815 di @raymondk — aperta

Valutazione

Difficoltà
5/5
Tempo stimato
Più di una settimana
Idoneità per principianti
35/100
Tipo di issue
Funzionalità
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
rust
Ambito
cli

Direzione di ricerca

Read the existing crates/icp-canister-interfaces cycles-ledger interface and commands/cycles/mint.rs and transfer.rs first; the proposed entry point is commands/cycles/buy.rs. The issue specifies gateway methods, UX, polling, and error handling, but leaves decisions such as the default gateway and command naming to maintainers. Done means implementing the agreed command and its tests for order creation, checkout output, waiting or resume, and gateway errors.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

enhancement

Summary

Add icp cycles buy: purchase cycles with a credit card for the CLI's current identity, through an on-chain cycles gateway, with no ICP, no wallet and no browser sign-in. The command creates the order as the local identity, hands the hosted Stripe Checkout URL to the user, and waits until the cycles land on that identity's cycles-ledger account. The natural next command is icp deploy -e ic.

Today a developer with a card and no ICP has to: sign in to a web gateway with Internet Identity, pay, then enable CLI access in id.ai, run icp identity link web <name> --app <origin>, make it the default identity, and verify the principal matches, before icp deploy can spend what they bought. Every one of those steps exists only because the browser identity was the buyer. If the CLI identity is the buyer, all of them disappear.

This was validated end to end on 2026-10-07 against the CyclePay gateway (https://github.com/raymondk/cyclepay) on a local network with a real Stripe sandbox: a plain Ed25519 PEM identity → create_order → hosted Checkout → checkout.session.completed webhook → icrc1_transfer → 7.24 T cycles (minus the 100 M ledger fee) on the PEM principal's cycles-ledger account, driven by a shell wrapper over icp canister call. No gateway change was needed for the purchase itself.

Proposed UX

icp cycles buy --usd <amount> [-n ic | -e <env>] [--identity <name>]
               [--gateway <principal>] [--yes] [--no-open] [--no-wait] [--json] [-q]
icp cycles buy --resume <order-id> [...]      # continue waiting on an order

Interactive run:

$ icp cycles buy --usd 10 -n ic
Buyer      rv4ez-fgjbj-...-vae  (identity: default)
Quote      $10.00 → 7.238 T cycles  (card processing $0.59, rate locked at order creation)
Proceed? [y/N] y
Order      26dd22b2cdf36f27d316ca5d0d19b39e  (payable for 35 min)

Pay here:  https://checkout.stripe.com/c/pay/cs_test_...

Waiting for payment ⠋  created
                       paid
                       delivered
Delivered  7.238 T cycles to rv4ez-fgjbj-...-vae  (ledger block 1234)
Balance    7.238 T cycles
Next       icp deploy -e ic

Behaviour:

  • Always print the URL, and try open::that only on a TTY unless --no-open. Agents, SSH sessions and containers have no browser; the user commonly pays on another device. The payment happens entirely on Stripe's hosted page; the CLI never sees card data.
  • --yes skips the confirmation; --json emits one object after creation (order_id, checkout_url, usd_cents, locked_cycles, expires_at) and one at the end (status, cycles_delivered, block_index); -q prints only the URL, then only the final status.
  • Ctrl-C leaves the order payable until its Stripe deadline and prints icp cycles buy --resume <id> and the cancel hint. --no-wait returns right after printing the URL.
  • Poll get_order every ~3 s. Terminal states: delivered (exit 0), expired / cancelled / abandoned (exit 1), needsReview (exit 1 with "your card was charged; the gateway operator will resolve delivery").
  • Pin the quoted quantity: pass minCycles = quote × 0.95 so a rate move against the buyer between quote and creation is refused with quoteChanged { quoted; minimum } instead of silently delivering less; on that error, re-quote and re-confirm. A move in the buyer's favour passes through.
  • Destination is always the caller's own default-subaccount cycles-ledger account. The gateway refuses anything else (destinationNotOwned), so there is no --to flag. To fund a canister afterwards: icp canister top-up.

Gateway interface

The gateway is a Motoko canister; mainnet backend saz2a-riaaa-aaaay-aadha-cai (the frontend shy4u-4qaaa-aaaay-aadhq-cai serves https://cyclepay.raymondk.co). Five methods are needed; the full .did is src/backend/dist/backend.did in the gateway repo.

type Amount      = variant { custom : nat; tier : text };          // USD cents, or a preset id
type Account     = record { owner : principal; subaccount : opt blob };
type Destination = variant { cyclesLedgerAccount : Account };
type OrderStatus = variant { created; paid; delivered; expired; cancelled; needsReview; abandoned };
type Order = record {
  id : text; status : OrderStatus; lockedCycles : nat;
  stripeSessionUrl : opt text; expiresAtNs : opt int;
  paidUsdCents : opt nat; expiredBy : opt variant { sessionExpired; sessionFailed };
  /* …pricing, destination, owner, problems… */
};
type QuotePreview  = record { usdCents : nat; feeCents : nat; netCents : opt nat; cycles : opt nat };
type QuotePreviews = record { quotes : vec QuotePreview; rates : opt Rates };
type Reason = variant {                                         // admission refusals
  amountAboveMax : record { usdCents : nat; maxUsdCents : nat };
  amountBelowMin : record { usdCents : nat; minUsdCents : nat };
  tooManyOpenOrders : record { open : nat; max : nat };
  canisterCyclesLow : record { balance : nat; minimum : nat };
  reserveShort : record { available : nat; needed : nat };
  unboundedGiveaway; buyerNotAllowed;
};
type CreateOrderError = variant {
  anonymous; unknownTier : text; tierBelowFees : text;
  simulationScaleTooSmall : record { scaledCycles : nat; ledgerFee : nat };
  reserveUnavailable; rateUnavailable;
  quoteChanged : record { quoted : nat; minimum : nat };
  idGeneration; notAdmitted : Reason; destinationNotOwned;
  sessionUnavailable : text; cancelledDuringCreation;
};
service : {
  quote_previews : (amounts : vec nat) -> (QuotePreviews) query;            // public
  can_purchase   : (usdCents : nat) -> (variant { ok; err : Reason }) query;  // advisory preflight
  create_order   : (Amount, Destination, minCycles : opt nat)
                   -> (variant { ok : record { order : Order }; err : CreateOrderError });
  get_order      : (id : text) -> (opt Order) query;                        // owner-scoped
  cancel_order   : (id : text) -> (variant { ok : Order; err : CancelOrderError });
}

(Record field names inside Reason abbreviated above; take them from the .did.) quote_previews is computed by the same function that prices the order, so the CLI should display its figure rather than compute one.

Suggested home: a typed interface module in crates/icp-canister-interfaces (same shape as cycles_ledger.rs), and a commands/cycles/buy.rs beside mint.rs / transfer.rs. open is already a workspace dependency (used by identity link web), as are indicatif / render/spinner.rs for the wait.

Error mapping

gateway answer CLI message / action
notAdmitted(amountBelowMin / amountAboveMax) state both figures; exit 1
notAdmitted(tooManyOpenOrders) the identity already has a payable order; print its id, offer --resume <id> or cancel_order; do not create another
notAdmitted(reserveShort) the gateway cannot cover this amount right now; a smaller amount may succeed
notAdmitted(canisterCyclesLow / unboundedGiveaway), reserveUnavailable, rateUnavailable, sessionUnavailable gateway-side, not the buyer's fault; retry later
notAdmitted(buyerNotAllowed) this gateway is in simulation mode and only sells to allow-listed principals
quoteChanged { quoted; minimum } re-quote, show the new figure, re-confirm
cancelledDuringCreation an order was cancelled from elsewhere mid-creation; start over
anonymous, destinationNotOwned, unknownTier, tierBelowFees, simulationScaleTooSmall, idGeneration not reachable from this command when it builds the arguments itself; report verbatim

Open questions for maintainers

  1. Default gateway. --gateway <principal> with a default: hardcoded, or an icp settings key? A gateway is a third party taking card payments, so whichever default ships is an endorsement decision rather than a technical one.
  2. Name. cycles buy reads naturally beside cycles mint; cycles purchase is the alternative.
  3. --cycles <amount> as an alternative to --usd, resolved to the smallest USD amount whose quote clears it. Useful for "I need 5 T to deploy"; can be a follow-up.
  4. Tolerance. 5 % matches the gateway's own web frontend; should it be a flag?
  5. Gateway readiness. The mainnet CyclePay deployment currently runs in simulation mode (Stripe sandbox, allow-listed buyers, a scaling divisor), tracked in the gateway repo. The CLI side is independent of that.

Related

Lingua principale
Rust
Stelle
115
Fork
14
Merge medio
21h 31m
PR unite (30g)
28

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di dfinity/icp-cli

Tutte le issue di dfinity/icp-cli

Issue simili

Altre issue su Rust

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.