cspr402 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +108 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +207 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +400 -0
- package/dist/client.js.map +1 -0
- package/dist/commands/onboard.d.ts +4 -0
- package/dist/commands/onboard.d.ts.map +1 -0
- package/dist/commands/onboard.js +192 -0
- package/dist/commands/onboard.js.map +1 -0
- package/dist/commands/onboard.test.d.ts +2 -0
- package/dist/commands/onboard.test.d.ts.map +1 -0
- package/dist/commands/onboard.test.js +48 -0
- package/dist/commands/onboard.test.js.map +1 -0
- package/dist/commands/purchase.d.ts +2 -0
- package/dist/commands/purchase.d.ts.map +1 -0
- package/dist/commands/purchase.js +206 -0
- package/dist/commands/purchase.js.map +1 -0
- package/dist/commands/wallet.d.ts +2 -0
- package/dist/commands/wallet.d.ts.map +1 -0
- package/dist/commands/wallet.js +161 -0
- package/dist/commands/wallet.js.map +1 -0
- package/dist/config.d.ts +77 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +329 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +101 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +197 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +337 -0
- package/dist/mcp.js.map +1 -0
- package/dist/mpp.d.ts +57 -0
- package/dist/mpp.d.ts.map +1 -0
- package/dist/mpp.js +165 -0
- package/dist/mpp.js.map +1 -0
- package/dist/ows.d.ts +190 -0
- package/dist/ows.d.ts.map +1 -0
- package/dist/ows.js +565 -0
- package/dist/ows.js.map +1 -0
- package/dist/soroban.d.ts +92 -0
- package/dist/soroban.d.ts.map +1 -0
- package/dist/soroban.js +313 -0
- package/dist/soroban.js.map +1 -0
- package/dist/stellar.d.ts +53 -0
- package/dist/stellar.d.ts.map +1 -0
- package/dist/stellar.js +180 -0
- package/dist/stellar.js.map +1 -0
- package/dist/version-check.d.ts +5 -0
- package/dist/version-check.d.ts.map +1 -0
- package/dist/version-check.js +203 -0
- package/dist/version-check.js.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { rpc, xdr, type Transaction } from '@stellar/stellar-sdk';
|
|
2
|
+
export declare function getSorobanRpcUrl(networkPassphrase: string): string;
|
|
3
|
+
export declare function getHorizonUrl(networkPassphrase?: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Convert a decimal string like "10.00" or "1.2345678" to a 7-decimal i128
|
|
6
|
+
* bigint (stroops / micro-USDC). Rejects inputs with >7 fractional digits.
|
|
7
|
+
*/
|
|
8
|
+
export declare function decimalToStroops(decimal: string): bigint;
|
|
9
|
+
/**
|
|
10
|
+
* Thrown when Soroban rejects a transaction because the offered fee was
|
|
11
|
+
* too low. `requiredFee` is the minimum the network demanded (in stroops),
|
|
12
|
+
* taken directly from the error response — callers should use it as the
|
|
13
|
+
* floor when rebuilding the transaction.
|
|
14
|
+
*/
|
|
15
|
+
export declare class InsufficientFeeError extends Error {
|
|
16
|
+
requiredFee: string;
|
|
17
|
+
constructor(requiredFee: string);
|
|
18
|
+
}
|
|
19
|
+
export type PaymentFn = 'pay_usdc' | 'pay_xlm';
|
|
20
|
+
export interface BuildContractTxOpts {
|
|
21
|
+
contractId: string;
|
|
22
|
+
fn: PaymentFn;
|
|
23
|
+
fromPublicKey: string;
|
|
24
|
+
amountStroops: bigint;
|
|
25
|
+
orderId: string;
|
|
26
|
+
networkPassphrase: string;
|
|
27
|
+
rpcUrl?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Force the built tx to use this exact sequence number instead of
|
|
30
|
+
* whatever Soroban RPC reports as the account's current sequence.
|
|
31
|
+
*
|
|
32
|
+
* Used by payViaContractOWS's retry loop to guarantee mutual
|
|
33
|
+
* exclusion with a prior failed submit. If the original tx ends
|
|
34
|
+
* up landing (e.g. it was accepted by the network despite the
|
|
35
|
+
* client-side 120s deadline expiring), the retry — which shares
|
|
36
|
+
* the same sequence — will fail with `tx_bad_seq` rather than
|
|
37
|
+
* landing as a second payment. That gives us a free double-pay
|
|
38
|
+
* safety property: at most one of the two attempts can ever
|
|
39
|
+
* credit the order.
|
|
40
|
+
*
|
|
41
|
+
* Pass the sequence number that should appear in the built tx
|
|
42
|
+
* (i.e. the post-bump value captured from a prior Transaction's
|
|
43
|
+
* `.sequence` field). The builder internally converts that into
|
|
44
|
+
* a pre-bump Account so the TransactionBuilder's
|
|
45
|
+
* incrementSequenceNumber() call lands on exactly this value.
|
|
46
|
+
*/
|
|
47
|
+
preservedSequence?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Override the fee (stroops). Defaults to BASE_FEE. Set this to the
|
|
50
|
+
* `requiredFee` from an InsufficientFeeError to retry with the fee
|
|
51
|
+
* floor the network actually demanded.
|
|
52
|
+
*/
|
|
53
|
+
fee?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build a Soroban transaction that invokes the receiver contract's pay_usdc
|
|
57
|
+
* or pay_xlm function. Returns an unsigned, simulation-prepared Transaction
|
|
58
|
+
* ready for the caller to sign (either via keypair or OWS) and submit.
|
|
59
|
+
*/
|
|
60
|
+
export declare function buildContractPaymentTx(opts: BuildContractTxOpts): Promise<{
|
|
61
|
+
tx: Transaction;
|
|
62
|
+
server: rpc.Server;
|
|
63
|
+
}>;
|
|
64
|
+
/**
|
|
65
|
+
* Submit a signed Soroban transaction and poll until it reaches a terminal
|
|
66
|
+
* status. Returns the transaction hash on success.
|
|
67
|
+
*
|
|
68
|
+
* Important: a successful return only guarantees the tx has been accepted
|
|
69
|
+
* onto the ledger. The cards402 backend's contract watcher is the source
|
|
70
|
+
* of truth for "the order has been credited" — the SDK's purchaseCardOWS
|
|
71
|
+
* always follows up with waitForCard against the order id, so even if
|
|
72
|
+
* this function gives up before finalization, the watcher still has a
|
|
73
|
+
* chance to credit the order if the tx eventually lands.
|
|
74
|
+
*/
|
|
75
|
+
export declare function submitSorobanTx(tx: Transaction, server: rpc.Server, horizonUrl?: string): Promise<string>;
|
|
76
|
+
/**
|
|
77
|
+
* Decide which contract function + asset amount to use based on the
|
|
78
|
+
* PaymentInstructions returned from POST /v1/orders.
|
|
79
|
+
*/
|
|
80
|
+
export declare function selectContractCall(payment: {
|
|
81
|
+
usdc: {
|
|
82
|
+
amount: string;
|
|
83
|
+
};
|
|
84
|
+
xlm?: {
|
|
85
|
+
amount: string;
|
|
86
|
+
};
|
|
87
|
+
}, paymentAsset: 'usdc' | 'xlm'): {
|
|
88
|
+
fn: PaymentFn;
|
|
89
|
+
amountDecimal: string;
|
|
90
|
+
};
|
|
91
|
+
export { xdr };
|
|
92
|
+
//# sourceMappingURL=soroban.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"soroban.d.ts","sourceRoot":"","sources":["../src/soroban.ts"],"names":[],"mappings":"AAKA,OAAO,EAQL,GAAG,EACH,GAAG,EACH,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAO9B,wBAAgB,gBAAgB,CAAC,iBAAiB,EAAE,MAAM,GAAG,MAAM,CAElE;AAOD,wBAAgB,aAAa,CAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAqBxD;AAED;;;;;GAKG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,WAAW,EAAE,MAAM,CAAC;gBACR,WAAW,EAAE,MAAM;CAIhC;AAED,MAAM,MAAM,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;AAE/C,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,SAAS,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;;;;;;;;;OAkBG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC;IAAE,EAAE,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAA;CAAE,CAAC,CAkClD;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CACnC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,GAAG,CAAC,MAAM,EAMlB,UAAU,GAAE,MAAwB,GACnC,OAAO,CAAC,MAAM,CAAC,CAoMjB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE;IAAE,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,GAAG,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,EAC/D,YAAY,EAAE,MAAM,GAAG,KAAK,GAC3B;IAAE,EAAE,EAAE,SAAS,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAQ1C;AAID,OAAO,EAAE,GAAG,EAAE,CAAC"}
|
package/dist/soroban.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Soroban contract payment helpers — shared by raw-keypair (stellar.ts) and
|
|
3
|
+
// OWS-wallet (ows.ts) payment paths. The receiver contract's pay_usdc/pay_xlm
|
|
4
|
+
// functions take (from: Address, amount: i128, order_id: Bytes) and emit a
|
|
5
|
+
// payment event that the cards402 backend watcher indexes.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.xdr = exports.InsufficientFeeError = void 0;
|
|
8
|
+
exports.getSorobanRpcUrl = getSorobanRpcUrl;
|
|
9
|
+
exports.getHorizonUrl = getHorizonUrl;
|
|
10
|
+
exports.decimalToStroops = decimalToStroops;
|
|
11
|
+
exports.buildContractPaymentTx = buildContractPaymentTx;
|
|
12
|
+
exports.submitSorobanTx = submitSorobanTx;
|
|
13
|
+
exports.selectContractCall = selectContractCall;
|
|
14
|
+
const stellar_sdk_1 = require("@stellar/stellar-sdk");
|
|
15
|
+
Object.defineProperty(exports, "xdr", { enumerable: true, get: function () { return stellar_sdk_1.xdr; } });
|
|
16
|
+
const MAINNET_RPC = 'https://mainnet.sorobanrpc.com';
|
|
17
|
+
const TESTNET_RPC = 'https://soroban-testnet.stellar.org';
|
|
18
|
+
const MAINNET_HORIZON = 'https://horizon.stellar.org';
|
|
19
|
+
const TESTNET_HORIZON = 'https://horizon-testnet.stellar.org';
|
|
20
|
+
function getSorobanRpcUrl(networkPassphrase) {
|
|
21
|
+
return networkPassphrase === stellar_sdk_1.Networks.TESTNET ? TESTNET_RPC : MAINNET_RPC;
|
|
22
|
+
}
|
|
23
|
+
// F3-soroban (2026-04-16): network-aware Horizon URL. Pre-fix, every
|
|
24
|
+
// Horizon fallback in submitSorobanTx was hardcoded to mainnet.
|
|
25
|
+
// Testnet agents got 404s from mainnet Horizon for their testnet txs,
|
|
26
|
+
// which the SDK interpreted as "tx never applied" with `dropped: true`
|
|
27
|
+
// — triggering the retry loop and risking a double-payment.
|
|
28
|
+
function getHorizonUrl(networkPassphrase) {
|
|
29
|
+
return networkPassphrase === stellar_sdk_1.Networks.TESTNET ? TESTNET_HORIZON : MAINNET_HORIZON;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Convert a decimal string like "10.00" or "1.2345678" to a 7-decimal i128
|
|
33
|
+
* bigint (stroops / micro-USDC). Rejects inputs with >7 fractional digits.
|
|
34
|
+
*/
|
|
35
|
+
function decimalToStroops(decimal) {
|
|
36
|
+
if (!/^\d+(\.\d+)?$/.test(decimal)) {
|
|
37
|
+
throw new Error(`Invalid decimal amount: ${decimal}`);
|
|
38
|
+
}
|
|
39
|
+
const parts = decimal.split('.');
|
|
40
|
+
const whole = parts[0] ?? '0';
|
|
41
|
+
const frac = parts[1] ?? '';
|
|
42
|
+
if (frac.length > 7) {
|
|
43
|
+
throw new Error(`Amount has more than 7 decimal places: ${decimal}`);
|
|
44
|
+
}
|
|
45
|
+
const padded = frac.padEnd(7, '0');
|
|
46
|
+
const stroops = BigInt(whole) * 10000000n + BigInt(padded || '0');
|
|
47
|
+
// F1-soroban (2026-04-16): reject zero amounts early. A pay_usdc(0)
|
|
48
|
+
// or pay_xlm(0) invocation costs gas, clutters the ledger with a
|
|
49
|
+
// zero-value event, and the backend watcher dead-letters it (the
|
|
50
|
+
// F4-stellar audit rejects non-positive i128 at parse time). Catching
|
|
51
|
+
// it here saves the agent gas and avoids a confusing dead-letter row.
|
|
52
|
+
if (stroops <= 0n) {
|
|
53
|
+
throw new Error(`Amount must be positive: ${decimal}`);
|
|
54
|
+
}
|
|
55
|
+
return stroops;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Thrown when Soroban rejects a transaction because the offered fee was
|
|
59
|
+
* too low. `requiredFee` is the minimum the network demanded (in stroops),
|
|
60
|
+
* taken directly from the error response — callers should use it as the
|
|
61
|
+
* floor when rebuilding the transaction.
|
|
62
|
+
*/
|
|
63
|
+
class InsufficientFeeError extends Error {
|
|
64
|
+
requiredFee;
|
|
65
|
+
constructor(requiredFee) {
|
|
66
|
+
super(`Soroban transaction rejected: fee too low (network requires ${requiredFee} stroops)`);
|
|
67
|
+
this.requiredFee = requiredFee;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.InsufficientFeeError = InsufficientFeeError;
|
|
71
|
+
/**
|
|
72
|
+
* Build a Soroban transaction that invokes the receiver contract's pay_usdc
|
|
73
|
+
* or pay_xlm function. Returns an unsigned, simulation-prepared Transaction
|
|
74
|
+
* ready for the caller to sign (either via keypair or OWS) and submit.
|
|
75
|
+
*/
|
|
76
|
+
async function buildContractPaymentTx(opts) {
|
|
77
|
+
const server = new stellar_sdk_1.rpc.Server(opts.rpcUrl ?? getSorobanRpcUrl(opts.networkPassphrase));
|
|
78
|
+
// When preservedSequence is set, build the Account manually from
|
|
79
|
+
// the pre-bump value (N-1) so TransactionBuilder's implicit
|
|
80
|
+
// incrementSequenceNumber() call lands exactly on the requested N.
|
|
81
|
+
// Otherwise, load the current on-chain state.
|
|
82
|
+
const account = opts.preservedSequence
|
|
83
|
+
? new stellar_sdk_1.Account(opts.fromPublicKey, (BigInt(opts.preservedSequence) - 1n).toString())
|
|
84
|
+
: await server.getAccount(opts.fromPublicKey);
|
|
85
|
+
const contract = new stellar_sdk_1.Contract(opts.contractId);
|
|
86
|
+
const orderIdBytes = Buffer.from(opts.orderId, 'utf-8');
|
|
87
|
+
const op = contract.call(opts.fn, new stellar_sdk_1.Address(opts.fromPublicKey).toScVal(), (0, stellar_sdk_1.nativeToScVal)(opts.amountStroops, { type: 'i128' }), (0, stellar_sdk_1.nativeToScVal)(orderIdBytes, { type: 'bytes' }));
|
|
88
|
+
const raw = new stellar_sdk_1.TransactionBuilder(account, {
|
|
89
|
+
fee: opts.fee ?? stellar_sdk_1.BASE_FEE,
|
|
90
|
+
networkPassphrase: opts.networkPassphrase,
|
|
91
|
+
})
|
|
92
|
+
.addOperation(op)
|
|
93
|
+
.setTimeout(180)
|
|
94
|
+
.build();
|
|
95
|
+
const sim = await server.simulateTransaction(raw);
|
|
96
|
+
if (stellar_sdk_1.rpc.Api.isSimulationError(sim)) {
|
|
97
|
+
throw new Error(`Soroban simulation failed: ${sim.error}`);
|
|
98
|
+
}
|
|
99
|
+
const prepared = stellar_sdk_1.rpc.assembleTransaction(raw, sim).build();
|
|
100
|
+
return { tx: prepared, server };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Submit a signed Soroban transaction and poll until it reaches a terminal
|
|
104
|
+
* status. Returns the transaction hash on success.
|
|
105
|
+
*
|
|
106
|
+
* Important: a successful return only guarantees the tx has been accepted
|
|
107
|
+
* onto the ledger. The cards402 backend's contract watcher is the source
|
|
108
|
+
* of truth for "the order has been credited" — the SDK's purchaseCardOWS
|
|
109
|
+
* always follows up with waitForCard against the order id, so even if
|
|
110
|
+
* this function gives up before finalization, the watcher still has a
|
|
111
|
+
* chance to credit the order if the tx eventually lands.
|
|
112
|
+
*/
|
|
113
|
+
async function submitSorobanTx(tx, server,
|
|
114
|
+
// F3-soroban (2026-04-16): callers pass the network-aware Horizon URL
|
|
115
|
+
// so the fallback checks hit the correct network. Pre-fix, all three
|
|
116
|
+
// fetch sites below were hardcoded to mainnet. Default to mainnet for
|
|
117
|
+
// backward compat — callers that don't pass this parameter get the
|
|
118
|
+
// same behaviour as before.
|
|
119
|
+
horizonUrl = MAINNET_HORIZON) {
|
|
120
|
+
// sendTransaction is idempotent for the same envelope. Three cases we
|
|
121
|
+
// retry explicitly:
|
|
122
|
+
// - TRY_AGAIN_LATER: RPC is congested, re-send after a short wait
|
|
123
|
+
// - thrown network error (fetch/DNS/TCP): the request never reached
|
|
124
|
+
// the RPC, safe to retry
|
|
125
|
+
// - DUPLICATE is treated as "already in flight" — fall through to
|
|
126
|
+
// the polling loop below instead of looping forever
|
|
127
|
+
// Up to 5 total attempts with 1.5s spacing covers typical RPC drops.
|
|
128
|
+
const MAX_SEND_ATTEMPTS = 5;
|
|
129
|
+
let send;
|
|
130
|
+
let sendErr;
|
|
131
|
+
for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt++) {
|
|
132
|
+
try {
|
|
133
|
+
send = await server.sendTransaction(tx);
|
|
134
|
+
if (send.status === 'TRY_AGAIN_LATER') {
|
|
135
|
+
if (attempt < MAX_SEND_ATTEMPTS - 1) {
|
|
136
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
throw new Error(`Soroban network congested — sendTransaction returned TRY_AGAIN_LATER after ${MAX_SEND_ATTEMPTS} attempts. Retry the purchase in a minute.`);
|
|
140
|
+
}
|
|
141
|
+
if (send.status === 'ERROR') {
|
|
142
|
+
// txInsufficientFee — the network tells us what fee it needed.
|
|
143
|
+
// Surface it as InsufficientFeeError so callers can rebuild
|
|
144
|
+
// with the required fee as the floor and retry immediately.
|
|
145
|
+
try {
|
|
146
|
+
if (send.errorResult?.result().switch().name === 'txInsufficientFee') {
|
|
147
|
+
throw new InsufficientFeeError(send.errorResult.feeCharged().toString());
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (feeErr) {
|
|
151
|
+
if (feeErr instanceof InsufficientFeeError)
|
|
152
|
+
throw feeErr;
|
|
153
|
+
// XDR parsing failed — fall through to the generic error.
|
|
154
|
+
}
|
|
155
|
+
throw new Error(`Soroban sendTransaction error: ${JSON.stringify(send.errorResult ?? send)}`);
|
|
156
|
+
}
|
|
157
|
+
// PENDING or DUPLICATE — envelope is now known to the network. Break
|
|
158
|
+
// out of the retry loop and poll for finalization.
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
sendErr = err;
|
|
163
|
+
// If the thrown error is structured (TRY_AGAIN_LATER, ERROR) let it
|
|
164
|
+
// propagate directly. Otherwise assume it was a transient network
|
|
165
|
+
// failure and retry.
|
|
166
|
+
if (err instanceof InsufficientFeeError ||
|
|
167
|
+
(err instanceof Error &&
|
|
168
|
+
(err.message.startsWith('Soroban network congested') ||
|
|
169
|
+
err.message.startsWith('Soroban sendTransaction error')))) {
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
if (attempt >= MAX_SEND_ATTEMPTS - 1) {
|
|
173
|
+
throw new Error(`Soroban sendTransaction failed after ${MAX_SEND_ATTEMPTS} attempts: ${err?.message ?? String(err)}`);
|
|
174
|
+
}
|
|
175
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!send) {
|
|
179
|
+
throw new Error(`Soroban sendTransaction produced no result: ${sendErr?.message ?? 'unknown error'}`);
|
|
180
|
+
}
|
|
181
|
+
// Poll for finalization. 120s deadline (was 60s) so we don't bail on
|
|
182
|
+
// mainnet RPC under modest backpressure. Each poll is cheap; the cost
|
|
183
|
+
// of a longer wait is much less than the cost of a stranded order.
|
|
184
|
+
// Some stellar-sdk versions can't parse newer Soroban RPC response XDR
|
|
185
|
+
// ("Bad union switch: 4"); when that happens, fall back to Horizon.
|
|
186
|
+
//
|
|
187
|
+
// Error-handling contract — changed 2026-04-14 after we shipped a
|
|
188
|
+
// stranded-order bug where FAILED statuses from getTransaction were
|
|
189
|
+
// silently caught by the inner try/catch and the loop kept polling
|
|
190
|
+
// until the deadline, at which point a txHash-attached error was
|
|
191
|
+
// thrown and purchaseCardOWS fell through to waitForCard on a tx
|
|
192
|
+
// that had actually failed on-chain. The new rules are:
|
|
193
|
+
//
|
|
194
|
+
// - Any throw whose message starts with "Soroban transaction " is
|
|
195
|
+
// one of OUR OWN terminal throws; the catch re-raises it out of
|
|
196
|
+
// the while loop unchanged.
|
|
197
|
+
// - A `txHash` is only attached to the final timeout error when we
|
|
198
|
+
// genuinely believe the tx MAY still land on the ledger later
|
|
199
|
+
// (Horizon unreachable). In every other terminal state — tx
|
|
200
|
+
// applied and failed, or Horizon confirms the tx never made a
|
|
201
|
+
// ledger — we throw a plain Error, which purchaseCardOWS will
|
|
202
|
+
// propagate to the caller instead of entering waitForCard.
|
|
203
|
+
const POLL_DEADLINE_MS = 120_000;
|
|
204
|
+
const deadline = Date.now() + POLL_DEADLINE_MS;
|
|
205
|
+
while (Date.now() < deadline) {
|
|
206
|
+
try {
|
|
207
|
+
const status = await server.getTransaction(send.hash);
|
|
208
|
+
if (status.status === 'SUCCESS')
|
|
209
|
+
return send.hash;
|
|
210
|
+
if (status.status === 'FAILED') {
|
|
211
|
+
throw new Error(`Soroban transaction ${send.hash} failed on-chain`);
|
|
212
|
+
}
|
|
213
|
+
// NOT_FOUND (or any transient non-terminal status): keep polling.
|
|
214
|
+
}
|
|
215
|
+
catch (pollErr) {
|
|
216
|
+
// Re-raise our own terminal throws so they escape the loop.
|
|
217
|
+
if (pollErr instanceof Error &&
|
|
218
|
+
pollErr.message.startsWith(`Soroban transaction ${send.hash}`)) {
|
|
219
|
+
throw pollErr;
|
|
220
|
+
}
|
|
221
|
+
// "Bad union switch" = XDR version mismatch between SDK and RPC.
|
|
222
|
+
// The TX was accepted by sendTransaction; confirm via Horizon instead.
|
|
223
|
+
if (String(pollErr?.message || '').includes('Bad union switch')) {
|
|
224
|
+
try {
|
|
225
|
+
const horizonResp = await fetch(`${horizonUrl}/transactions/${send.hash}`);
|
|
226
|
+
if (horizonResp.ok) {
|
|
227
|
+
const horizonData = (await horizonResp.json());
|
|
228
|
+
if (horizonData.successful)
|
|
229
|
+
return send.hash;
|
|
230
|
+
// Horizon can see it and it failed — terminal, no recovery.
|
|
231
|
+
throw new Error(`Soroban transaction ${send.hash} failed on-chain (Horizon)`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (horizonErr) {
|
|
235
|
+
// Re-raise our own terminal Horizon throws
|
|
236
|
+
if (horizonErr instanceof Error &&
|
|
237
|
+
horizonErr.message.startsWith(`Soroban transaction ${send.hash}`)) {
|
|
238
|
+
throw horizonErr;
|
|
239
|
+
}
|
|
240
|
+
/* Horizon unreachable — keep polling Soroban RPC */
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Any other poll-layer error: treat as transient, keep polling.
|
|
244
|
+
}
|
|
245
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
246
|
+
}
|
|
247
|
+
// Deadline reached without a terminal status. Before giving up, ask
|
|
248
|
+
// Horizon what actually happened to the tx — it's the authoritative
|
|
249
|
+
// record of what hit a ledger, independent of Soroban RPC's state.
|
|
250
|
+
try {
|
|
251
|
+
const horizonResp = await fetch(`${horizonUrl}/transactions/${send.hash}`);
|
|
252
|
+
if (horizonResp.ok) {
|
|
253
|
+
const horizonData = (await horizonResp.json());
|
|
254
|
+
if (horizonData.successful)
|
|
255
|
+
return send.hash;
|
|
256
|
+
// Tx landed and failed. No recovery path — throw without txHash
|
|
257
|
+
// so the caller propagates the error instead of waiting on a card
|
|
258
|
+
// that will never come.
|
|
259
|
+
throw new Error(`Soroban transaction ${send.hash} applied on-chain but failed — no card will be credited. Retry the purchase.`);
|
|
260
|
+
}
|
|
261
|
+
if (horizonResp.status === 404) {
|
|
262
|
+
// Tx was accepted by Soroban RPC (we have a hash) but it never
|
|
263
|
+
// made it into a ledger on Horizon. This is the "network rejected
|
|
264
|
+
// at apply time" case — e.g., source account sequence drifted,
|
|
265
|
+
// host function errored before any effects landed. It is NOT
|
|
266
|
+
// going to "eventually land".
|
|
267
|
+
//
|
|
268
|
+
// Attach both txHash and a structured `dropped: true` marker.
|
|
269
|
+
// payViaContractOWS's retry loop uses the marker to decide whether
|
|
270
|
+
// to resubmit with the same sequence (guaranteeing mutual
|
|
271
|
+
// exclusion). Without the marker, the retry layer can't
|
|
272
|
+
// distinguish this case from "on-chain failure" (where retrying
|
|
273
|
+
// would be incorrect because the sequence was consumed by the
|
|
274
|
+
// failed tx).
|
|
275
|
+
const droppedErr = new Error(`Soroban transaction ${send.hash} was accepted by the RPC but never applied on the ledger within ${POLL_DEADLINE_MS / 1000}s — the network rejected it pre-apply. Retry the purchase.`);
|
|
276
|
+
droppedErr.txHash = send.hash;
|
|
277
|
+
droppedErr.dropped = true;
|
|
278
|
+
throw droppedErr;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (horizonErr) {
|
|
282
|
+
// Re-raise our own terminal Horizon throws; swallow network errors.
|
|
283
|
+
if (horizonErr instanceof Error &&
|
|
284
|
+
horizonErr.message.startsWith(`Soroban transaction ${send.hash}`)) {
|
|
285
|
+
throw horizonErr;
|
|
286
|
+
}
|
|
287
|
+
/* Horizon itself is unreachable — fall through to the timeout
|
|
288
|
+
error below, where we attach txHash as a last-resort safety net.
|
|
289
|
+
The watcher may still credit the order if the tx eventually
|
|
290
|
+
lands and Horizon comes back up. */
|
|
291
|
+
}
|
|
292
|
+
// Soroban RPC never saw a terminal state AND Horizon is unreachable
|
|
293
|
+
// (or the response was neither 2xx nor 404). We genuinely don't know
|
|
294
|
+
// whether the tx landed, so attach txHash and let purchaseCardOWS
|
|
295
|
+
// give the cards402 backend watcher a chance to credit the order.
|
|
296
|
+
const err = new Error(`Soroban transaction ${send.hash} did not finalize within ${POLL_DEADLINE_MS / 1000}s and Horizon is unreachable`);
|
|
297
|
+
err.txHash = send.hash;
|
|
298
|
+
throw err;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Decide which contract function + asset amount to use based on the
|
|
302
|
+
* PaymentInstructions returned from POST /v1/orders.
|
|
303
|
+
*/
|
|
304
|
+
function selectContractCall(payment, paymentAsset) {
|
|
305
|
+
if (paymentAsset === 'xlm') {
|
|
306
|
+
if (!payment.xlm?.amount) {
|
|
307
|
+
throw new Error('Order response does not include an XLM quote');
|
|
308
|
+
}
|
|
309
|
+
return { fn: 'pay_xlm', amountDecimal: payment.xlm.amount };
|
|
310
|
+
}
|
|
311
|
+
return { fn: 'pay_usdc', amountDecimal: payment.usdc.amount };
|
|
312
|
+
}
|
|
313
|
+
//# sourceMappingURL=soroban.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"soroban.js","sourceRoot":"","sources":["../src/soroban.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Networks } from '@stellar/stellar-sdk';
|
|
2
|
+
import type { CardDetails, PaymentInstructions } from './client';
|
|
3
|
+
export interface WalletInfo {
|
|
4
|
+
publicKey: string;
|
|
5
|
+
secret: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function createWallet(): WalletInfo;
|
|
8
|
+
export declare function getBalance(publicKey: string, networkPassphrase?: string): Promise<{
|
|
9
|
+
xlm: string;
|
|
10
|
+
usdc: string;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function addUsdcTrustline(secret: string, networkPassphrase?: Networks): Promise<string>;
|
|
13
|
+
export interface PayOpts {
|
|
14
|
+
walletSecret: string;
|
|
15
|
+
payment: PaymentInstructions;
|
|
16
|
+
paymentAsset?: 'usdc' | 'xlm';
|
|
17
|
+
networkPassphrase?: string;
|
|
18
|
+
sorobanRpcUrl?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Pay the cards402 receiver contract using a raw Stellar secret key.
|
|
22
|
+
* Invokes pay_usdc or pay_xlm with the agent's G-address, the quoted amount
|
|
23
|
+
* converted to 7-decimal stroops, and the order_id from the create-order
|
|
24
|
+
* response. Returns the Soroban transaction hash.
|
|
25
|
+
*/
|
|
26
|
+
export declare function payViaContract(opts: PayOpts): Promise<string>;
|
|
27
|
+
/**
|
|
28
|
+
* Full purchase flow with a raw keypair: create order → invoke contract →
|
|
29
|
+
* wait for card. Pass `resume: { orderId, payment }` to re-enter a partially
|
|
30
|
+
* completed flow without creating a new order (S-9).
|
|
31
|
+
*/
|
|
32
|
+
export declare function purchaseCard(opts: {
|
|
33
|
+
apiKey: string;
|
|
34
|
+
walletSecret: string;
|
|
35
|
+
amountUsdc: string;
|
|
36
|
+
paymentAsset?: 'usdc' | 'xlm';
|
|
37
|
+
baseUrl?: string;
|
|
38
|
+
networkPassphrase?: string;
|
|
39
|
+
sorobanRpcUrl?: string;
|
|
40
|
+
resume?: {
|
|
41
|
+
orderId: string;
|
|
42
|
+
payment: PaymentInstructions;
|
|
43
|
+
};
|
|
44
|
+
waitForCardOpts?: {
|
|
45
|
+
timeoutMs?: number;
|
|
46
|
+
intervalMs?: number;
|
|
47
|
+
};
|
|
48
|
+
}): Promise<CardDetails & {
|
|
49
|
+
order_id: string;
|
|
50
|
+
}>;
|
|
51
|
+
/** @deprecated Use `payViaContract` — this is the Soroban contract call, not a direct Stellar payment. */
|
|
52
|
+
export declare const payVCC: typeof payViaContract;
|
|
53
|
+
//# sourceMappingURL=stellar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stellar.d.ts","sourceRoot":"","sources":["../src/stellar.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,QAAQ,EAOT,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAA8B,MAAM,UAAU,CAAC;AAuC7F,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,YAAY,IAAI,UAAU,CAGzC;AAED,wBAAsB,UAAU,CAC9B,SAAS,EAAE,MAAM,EACjB,iBAAiB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAexC;AAED,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,iBAAiB,WAAkB,GAClC,OAAO,CAAC,MAAM,CAAC,CAWjB;AAID,MAAM,WAAW,OAAO;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,mBAAmB,CAAC;IAC7B,YAAY,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CA6CnE;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,mBAAmB,CAAA;KAAE,CAAC;IAC3D,eAAe,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/D,GAAG,OAAO,CAAC,WAAW,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAgC9C;AAKD,0GAA0G;AAC1G,eAAO,MAAM,MAAM,uBAAiB,CAAC"}
|
package/dist/stellar.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Helpers for agents using a raw Stellar keypair (S...) to pay the cards402
|
|
3
|
+
// receiver contract on Soroban. For OWS-wallet custody, see ./ows.ts.
|
|
4
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
5
|
+
if (k2 === undefined) k2 = k;
|
|
6
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
7
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
8
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
9
|
+
}
|
|
10
|
+
Object.defineProperty(o, k2, desc);
|
|
11
|
+
}) : (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
o[k2] = m[k];
|
|
14
|
+
}));
|
|
15
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
16
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
17
|
+
}) : function(o, v) {
|
|
18
|
+
o["default"] = v;
|
|
19
|
+
});
|
|
20
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
21
|
+
var ownKeys = function(o) {
|
|
22
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
23
|
+
var ar = [];
|
|
24
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
25
|
+
return ar;
|
|
26
|
+
};
|
|
27
|
+
return ownKeys(o);
|
|
28
|
+
};
|
|
29
|
+
return function (mod) {
|
|
30
|
+
if (mod && mod.__esModule) return mod;
|
|
31
|
+
var result = {};
|
|
32
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
33
|
+
__setModuleDefault(result, mod);
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
})();
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.payVCC = void 0;
|
|
39
|
+
exports.createWallet = createWallet;
|
|
40
|
+
exports.getBalance = getBalance;
|
|
41
|
+
exports.addUsdcTrustline = addUsdcTrustline;
|
|
42
|
+
exports.payViaContract = payViaContract;
|
|
43
|
+
exports.purchaseCard = purchaseCard;
|
|
44
|
+
const stellar_sdk_1 = require("@stellar/stellar-sdk");
|
|
45
|
+
const soroban_1 = require("./soroban");
|
|
46
|
+
const USDC_ISSUER = 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN';
|
|
47
|
+
const HORIZON_TIMEOUT_MS = 15000;
|
|
48
|
+
function assertSorobanPayment(payment) {
|
|
49
|
+
if (payment.type !== 'soroban_contract') {
|
|
50
|
+
throw new Error(`Expected soroban_contract payment instructions, got ${payment.type}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function getHorizonUrl(networkPassphrase) {
|
|
54
|
+
return networkPassphrase === stellar_sdk_1.Networks.TESTNET
|
|
55
|
+
? 'https://horizon-testnet.stellar.org'
|
|
56
|
+
: 'https://horizon.stellar.org';
|
|
57
|
+
}
|
|
58
|
+
function getServer(networkPassphrase) {
|
|
59
|
+
return new stellar_sdk_1.Horizon.Server(getHorizonUrl(networkPassphrase));
|
|
60
|
+
}
|
|
61
|
+
function withTimeout(promise, ms = HORIZON_TIMEOUT_MS) {
|
|
62
|
+
return Promise.race([
|
|
63
|
+
promise,
|
|
64
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Horizon request timed out after ${ms}ms`)), ms)),
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
function createWallet() {
|
|
68
|
+
const keypair = stellar_sdk_1.Keypair.random();
|
|
69
|
+
return { publicKey: keypair.publicKey(), secret: keypair.secret() };
|
|
70
|
+
}
|
|
71
|
+
async function getBalance(publicKey, networkPassphrase) {
|
|
72
|
+
const server = getServer(networkPassphrase);
|
|
73
|
+
const account = await withTimeout(server.loadAccount(publicKey));
|
|
74
|
+
let xlm = '0', usdc = '0';
|
|
75
|
+
for (const b of account.balances) {
|
|
76
|
+
if (b.asset_type === 'native')
|
|
77
|
+
xlm = b.balance;
|
|
78
|
+
if (b.asset_type === 'credit_alphanum4' &&
|
|
79
|
+
b.asset_code === 'USDC' &&
|
|
80
|
+
b.asset_issuer === USDC_ISSUER)
|
|
81
|
+
usdc = b.balance;
|
|
82
|
+
}
|
|
83
|
+
return { xlm, usdc };
|
|
84
|
+
}
|
|
85
|
+
async function addUsdcTrustline(secret, networkPassphrase = stellar_sdk_1.Networks.PUBLIC) {
|
|
86
|
+
const server = getServer(networkPassphrase);
|
|
87
|
+
const keypair = stellar_sdk_1.Keypair.fromSecret(secret);
|
|
88
|
+
const account = await withTimeout(server.loadAccount(keypair.publicKey()));
|
|
89
|
+
const tx = new stellar_sdk_1.TransactionBuilder(account, { fee: stellar_sdk_1.BASE_FEE, networkPassphrase })
|
|
90
|
+
.addOperation(stellar_sdk_1.Operation.changeTrust({ asset: new stellar_sdk_1.Asset('USDC', USDC_ISSUER) }))
|
|
91
|
+
.setTimeout(300)
|
|
92
|
+
.build();
|
|
93
|
+
tx.sign(keypair);
|
|
94
|
+
const result = await server.submitTransaction(tx);
|
|
95
|
+
return result.hash;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Pay the cards402 receiver contract using a raw Stellar secret key.
|
|
99
|
+
* Invokes pay_usdc or pay_xlm with the agent's G-address, the quoted amount
|
|
100
|
+
* converted to 7-decimal stroops, and the order_id from the create-order
|
|
101
|
+
* response. Returns the Soroban transaction hash.
|
|
102
|
+
*/
|
|
103
|
+
async function payViaContract(opts) {
|
|
104
|
+
const { walletSecret, payment, paymentAsset = 'usdc', networkPassphrase = stellar_sdk_1.Networks.PUBLIC, sorobanRpcUrl, } = opts;
|
|
105
|
+
assertSorobanPayment(payment);
|
|
106
|
+
if (!stellar_sdk_1.StrKey.isValidContract(payment.contract_id)) {
|
|
107
|
+
throw new Error(`Invalid contract_id in order response: ${payment.contract_id}`);
|
|
108
|
+
}
|
|
109
|
+
const keypair = stellar_sdk_1.Keypair.fromSecret(walletSecret);
|
|
110
|
+
const { fn, amountDecimal } = (0, soroban_1.selectContractCall)(payment, paymentAsset);
|
|
111
|
+
const amountStroops = (0, soroban_1.decimalToStroops)(amountDecimal);
|
|
112
|
+
// Fee retry: if the network rejects the fee, rebuild with the
|
|
113
|
+
// required fee as the floor. At most one retry — the network's
|
|
114
|
+
// suggested fee should be sufficient on the second attempt.
|
|
115
|
+
let fee;
|
|
116
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
117
|
+
const { tx, server } = await (0, soroban_1.buildContractPaymentTx)({
|
|
118
|
+
contractId: payment.contract_id,
|
|
119
|
+
fn,
|
|
120
|
+
fromPublicKey: keypair.publicKey(),
|
|
121
|
+
amountStroops,
|
|
122
|
+
orderId: payment.order_id,
|
|
123
|
+
networkPassphrase,
|
|
124
|
+
rpcUrl: sorobanRpcUrl,
|
|
125
|
+
fee,
|
|
126
|
+
});
|
|
127
|
+
tx.sign(keypair);
|
|
128
|
+
try {
|
|
129
|
+
return await (0, soroban_1.submitSorobanTx)(tx, server, getHorizonUrl(networkPassphrase));
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
if (err instanceof soroban_1.InsufficientFeeError && attempt === 0) {
|
|
133
|
+
fee = err.requiredFee;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
throw new Error('payViaContract: fee retry exhausted');
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Full purchase flow with a raw keypair: create order → invoke contract →
|
|
143
|
+
* wait for card. Pass `resume: { orderId, payment }` to re-enter a partially
|
|
144
|
+
* completed flow without creating a new order (S-9).
|
|
145
|
+
*/
|
|
146
|
+
async function purchaseCard(opts) {
|
|
147
|
+
const { Cards402Client } = await Promise.resolve().then(() => __importStar(require('./client')));
|
|
148
|
+
const client = new Cards402Client({ apiKey: opts.apiKey, baseUrl: opts.baseUrl });
|
|
149
|
+
const paymentAsset = opts.paymentAsset ?? 'usdc';
|
|
150
|
+
let orderId;
|
|
151
|
+
let payment;
|
|
152
|
+
if (opts.resume) {
|
|
153
|
+
orderId = opts.resume.orderId;
|
|
154
|
+
payment = opts.resume.payment;
|
|
155
|
+
const status = await client.getOrder(orderId);
|
|
156
|
+
if (status.phase !== 'awaiting_payment') {
|
|
157
|
+
const card = await client.waitForCard(orderId, opts.waitForCardOpts);
|
|
158
|
+
return { ...card, order_id: orderId };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const order = await client.createOrder({ amount_usdc: opts.amountUsdc });
|
|
163
|
+
orderId = order.order_id;
|
|
164
|
+
payment = order.payment;
|
|
165
|
+
}
|
|
166
|
+
await payViaContract({
|
|
167
|
+
walletSecret: opts.walletSecret,
|
|
168
|
+
payment,
|
|
169
|
+
paymentAsset,
|
|
170
|
+
networkPassphrase: opts.networkPassphrase,
|
|
171
|
+
sorobanRpcUrl: opts.sorobanRpcUrl,
|
|
172
|
+
});
|
|
173
|
+
const card = await client.waitForCard(orderId, opts.waitForCardOpts);
|
|
174
|
+
return { ...card, order_id: orderId };
|
|
175
|
+
}
|
|
176
|
+
// Back-compat aliases — the pre-V3 SDK exposed these names. Keep them around
|
|
177
|
+
// as deprecated exports so existing imports don't break on upgrade.
|
|
178
|
+
/** @deprecated Use `payViaContract` — this is the Soroban contract call, not a direct Stellar payment. */
|
|
179
|
+
exports.payVCC = payViaContract;
|
|
180
|
+
//# sourceMappingURL=stellar.js.map
|