Feature: `icp cycles buy` — buy cycles with a card for the current identity via an on-chain gateway
メンテナーはふだん 1 日以内に返信
評価
調査の方向性
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.
索引モデルが issue の本文から書いたものです。
説明
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::thatonly 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. --yesskips the confirmation;--jsonemits one object after creation (order_id,checkout_url,usd_cents,locked_cycles,expires_at) and one at the end (status,cycles_delivered,block_index);-qprints 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-waitreturns right after printing the URL. - Poll
get_orderevery ~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.95so a rate move against the buyer between quote and creation is refused withquoteChanged { 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--toflag. 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
- Default gateway.
--gateway <principal>with a default: hardcoded, or anicp settingskey? A gateway is a third party taking card payments, so whichever default ships is an endorsement decision rather than a technical one. - Name.
cycles buyreads naturally besidecycles mint;cycles purchaseis the alternative. --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.- Tolerance. 5 % matches the gateway's own web frontend; should it be a flag?
- 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
- Gateway: return route for buyers who did not pay from the browser (the Stripe success page assumed the viewer owns the order): https://github.com/raymondk/cyclepay/pull/6
- Agent skill for this flow in
dfinity/icskills(draft, documents this command plus theicp canister callfallback): https://github.com/dfinity/icskills/pull/416
- 主要言語
- Rust
- スター
- 116
- フォーク
- 17
- 平均マージ
- 22時間 11分
- マージ済み PR(30日)
- 28
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
dfinity/icp-cli のほかの issue
-
難易度 3/5 1〜2日 初心者へのやさしさ 56/100
メンテナーはふだん 1 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 48/100
メンテナーはふだん 1 日以内に返信
-
難易度 3/5 1〜2日 初心者へのやさしさ 68/100
メンテナーはふだん 1 日以内に返信
-
sync plugin OOMs ("memory allocation of N bytes failed") when the target canister already has assetsオープン
難易度 4/5 3〜5日 初心者へのやさしさ 45/100
dfinity/icp-cli#703 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 3/5 1〜2日 初心者へのやさしさ 74/100
メンテナーはふだん 1 日以内に返信
dfinity/icp-cli の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
longbridge/gpui-kit#3620 ·
メンテナーはふだん 1 日以内に返信
-
corro-client drops the last byte of a response line that has no trailing newline対応中かも @RaphaelFakhri が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 88/100
-
proxy: include denial_category in the "proxy request denied" log line対応中かも @sreyas-endor が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 77/100
nolabs-ai/nono#2172 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
enhancement
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
metasequoiaime/msime#9113 ·
メンテナーはふだん 1 日以内に返信
-
[Enhancement] Make use of available space to display more processes when maximized in Memory pageオープン
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
pop-os/cosmic-monitor#126 ·
メンテナーはふだん 1 日以内に返信