@ic402/client 0.1.4 → 1.0.0
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/dist/client.d.ts +96 -22
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +295 -43
- package/dist/client.js.map +1 -1
- package/dist/evm.d.ts +107 -0
- package/dist/evm.d.ts.map +1 -0
- package/dist/evm.js +421 -0
- package/dist/evm.js.map +1 -0
- package/dist/idl.d.ts +25 -4
- package/dist/idl.d.ts.map +1 -1
- package/dist/idl.js +119 -31
- package/dist/idl.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +107 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
maxTotal?: bigint;
|
|
7
|
-
maxSessionDeposit?: bigint;
|
|
8
|
-
alertThreshold?: bigint;
|
|
1
|
+
/** Minimal identity interface — any ICP identity that can provide a principal. */
|
|
2
|
+
export interface Ic402Identity {
|
|
3
|
+
getPrincipal(): {
|
|
4
|
+
toText(): string;
|
|
5
|
+
};
|
|
9
6
|
}
|
|
7
|
+
import type { ContentDelivery, Job, PaymentReceipt, ServiceDefinition } from './types.js';
|
|
8
|
+
import { type VoucherSigner } from './voucher.js';
|
|
9
|
+
import { type FetchX402Result } from './evm.js';
|
|
10
10
|
export interface SessionPreferences {
|
|
11
11
|
preferSession?: boolean;
|
|
12
12
|
maxDeposit?: bigint;
|
|
@@ -22,26 +22,40 @@ export interface SessionPreferences {
|
|
|
22
22
|
evmToken?: string;
|
|
23
23
|
/** For EVM sessions: the canister's EVM address (settlement recipient). */
|
|
24
24
|
evmRecipient?: string;
|
|
25
|
+
/** For EVM sessions: the EIP-3009 authorization (signed by the payer). */
|
|
26
|
+
authorization?: {
|
|
27
|
+
from: string;
|
|
28
|
+
to: string;
|
|
29
|
+
value: number | bigint;
|
|
30
|
+
validAfter: number | bigint;
|
|
31
|
+
validBefore: number | bigint;
|
|
32
|
+
nonce: number[];
|
|
33
|
+
v: number;
|
|
34
|
+
r: number[];
|
|
35
|
+
s: number[];
|
|
36
|
+
};
|
|
25
37
|
}
|
|
26
38
|
export interface Ic402ClientConfig {
|
|
39
|
+
/** Target canister ID. Required for all operations. */
|
|
40
|
+
canisterId: string;
|
|
41
|
+
/** Factory to create actors for canister calls. Required for all operations. */
|
|
42
|
+
actorFactory: (canisterId: string) => any;
|
|
27
43
|
/** ICP identity for signing payments */
|
|
28
|
-
identity:
|
|
29
|
-
/** CAIP-2 network identifier */
|
|
44
|
+
identity: Ic402Identity | null;
|
|
45
|
+
/** CAIP-2 network identifier (e.g., "icp:1" for ICP, "eip155:84532" for Base Sepolia) */
|
|
30
46
|
network: string;
|
|
31
|
-
/** Automatically handle 402 responses */
|
|
47
|
+
/** Automatically handle 402 responses (ICP: ICRC-2 approve + retry) */
|
|
32
48
|
autoPayment?: boolean;
|
|
33
|
-
/** Budget limits */
|
|
34
|
-
budget?: BudgetConfig;
|
|
35
49
|
/** Session preferences */
|
|
36
50
|
sessions?: SessionPreferences;
|
|
37
|
-
/**
|
|
38
|
-
onBudgetAlert?: (spent: bigint, limit: bigint) => Promise<void>;
|
|
39
|
-
/** Ledger canister ID for ICRC-2 auto-approval */
|
|
51
|
+
/** Ledger canister ID for ICRC-2 auto-approval (ICP payments) */
|
|
40
52
|
ledger?: string;
|
|
41
|
-
/**
|
|
42
|
-
canisterId?: string;
|
|
43
|
-
/** Factory to create a ledger actor for ICRC-2 calls. Required for autoPayment. */
|
|
53
|
+
/** Factory for ledger actors. Required for ICP auto-payment. */
|
|
44
54
|
ledgerActorFactory?: (ledgerCanisterId: string) => any;
|
|
55
|
+
/** Custom EVM RPC URL. If omitted, uses a public RPC for the chain. */
|
|
56
|
+
evmRpcUrl?: string;
|
|
57
|
+
/** Fee buffer added to ICRC-2 approval amount (default: 100_000). */
|
|
58
|
+
approvalFeeBuffer?: bigint;
|
|
45
59
|
}
|
|
46
60
|
export interface SessionHandle {
|
|
47
61
|
id: string;
|
|
@@ -60,20 +74,19 @@ export interface SessionHandle {
|
|
|
60
74
|
*/
|
|
61
75
|
export declare class Ic402Client {
|
|
62
76
|
private config;
|
|
63
|
-
private totalSpent;
|
|
64
77
|
constructor(config: Ic402ClientConfig);
|
|
65
78
|
/**
|
|
66
79
|
* Call a canister method, auto-handling 402 payment if needed.
|
|
67
80
|
*
|
|
68
81
|
* Flow: call method → if #paymentRequired → icrc2_approve → create sig → retry
|
|
69
82
|
*/
|
|
70
|
-
call(
|
|
83
|
+
call(method: string, args: unknown[], canisterId?: string): Promise<unknown>;
|
|
71
84
|
/**
|
|
72
85
|
* Open a streaming session with escrow deposit.
|
|
73
86
|
*
|
|
74
87
|
* Flow: requestSession → calculate deposit → icrc2_approve → openSession → SessionHandle
|
|
75
88
|
*/
|
|
76
|
-
openSession(
|
|
89
|
+
openSession(sessionConfig?: Partial<SessionPreferences>, signer?: VoucherSigner, canisterId?: string): Promise<SessionHandle>;
|
|
77
90
|
/**
|
|
78
91
|
* Fetch content from a ContentDelivery response.
|
|
79
92
|
* Handles all delivery methods: inline, httpUrl, canisterQuery, assetCanister.
|
|
@@ -82,6 +95,67 @@ export declare class Ic402Client {
|
|
|
82
95
|
canisterId?: string;
|
|
83
96
|
actorFactory?: (canisterId: string) => any;
|
|
84
97
|
}): Promise<Uint8Array>;
|
|
98
|
+
/**
|
|
99
|
+
* Fetch from an x402-gated URL. The full flow:
|
|
100
|
+
* 1. Client probes the URL → gets 402 with payment requirements
|
|
101
|
+
* 2. Client calls canister signX402Payment → gets signed EIP-3009 header
|
|
102
|
+
* 3. Client retries the URL with the signed X-Payment header
|
|
103
|
+
*
|
|
104
|
+
* @param url - The x402-gated URL to fetch
|
|
105
|
+
* @param actorFactory - Factory to create canister actor
|
|
106
|
+
*/
|
|
107
|
+
fetchX402(url: string, options?: {
|
|
108
|
+
init?: RequestInit;
|
|
109
|
+
chainId?: number;
|
|
110
|
+
}): Promise<FetchX402Result>;
|
|
111
|
+
/**
|
|
112
|
+
* Register the canister as an agent on the ERC-8004 IdentityRegistry.
|
|
113
|
+
* Full flow: get chain state → canister signs → client broadcasts → poll receipt.
|
|
114
|
+
*
|
|
115
|
+
* @param actorFactory - Factory to create canister actor
|
|
116
|
+
* @param rpcUrl - Optional custom RPC URL for the target chain
|
|
117
|
+
*/
|
|
118
|
+
registerAgent(rpcUrl?: string, chainId?: number): Promise<{
|
|
119
|
+
tokenId: bigint | null;
|
|
120
|
+
txHash: string;
|
|
121
|
+
}>;
|
|
122
|
+
/**
|
|
123
|
+
* Sign and broadcast an ERC-20 transfer.
|
|
124
|
+
* Client fetches chain state, canister signs, client broadcasts.
|
|
125
|
+
*/
|
|
126
|
+
sendErc20Transfer(tokenAddress: string, recipient: string, amount: bigint, rpcUrl?: string): Promise<{
|
|
127
|
+
txHash: string;
|
|
128
|
+
}>;
|
|
129
|
+
/**
|
|
130
|
+
* Sign and broadcast a native ETH transfer.
|
|
131
|
+
*/
|
|
132
|
+
sendEthTransfer(recipient: string, amountWei: bigint, rpcUrl?: string): Promise<{
|
|
133
|
+
txHash: string;
|
|
134
|
+
}>;
|
|
135
|
+
/** Extract numeric chain ID from CAIP-2 network string, or null if not EVM. */
|
|
136
|
+
private tryExtractChainId;
|
|
137
|
+
/** Extract chain ID or throw. */
|
|
138
|
+
private extractChainId;
|
|
139
|
+
/**
|
|
140
|
+
* List available services from the canister.
|
|
141
|
+
*/
|
|
142
|
+
listServices(): Promise<ServiceDefinition[]>;
|
|
143
|
+
/**
|
|
144
|
+
* Submit a paid service request. Handles x402 payment automatically.
|
|
145
|
+
* Returns the job ID for polling.
|
|
146
|
+
*/
|
|
147
|
+
submitServiceRequest(serviceId: string, params: Uint8Array): Promise<{
|
|
148
|
+
jobId: string;
|
|
149
|
+
}>;
|
|
150
|
+
/**
|
|
151
|
+
* Poll for a job result until completed or max attempts reached.
|
|
152
|
+
* Returns the full job record when done.
|
|
153
|
+
*/
|
|
154
|
+
pollJobResult(jobId: string, maxAttempts?: number, intervalMs?: number): Promise<Job>;
|
|
155
|
+
/**
|
|
156
|
+
* Dispute a job result (for BuyerConfirm verification).
|
|
157
|
+
*/
|
|
158
|
+
disputeJob(jobId: string, reason: string): Promise<void>;
|
|
85
159
|
/**
|
|
86
160
|
* Discover agents via ERC-8004 registries.
|
|
87
161
|
*
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
import { Principal } from '@icp-sdk/core/principal';
|
|
2
|
+
/** JSON.stringify that handles BigInt values. */
|
|
3
|
+
function safeStringify(value) {
|
|
4
|
+
return JSON.stringify(value, (_key, val) => typeof val === 'bigint' ? val.toString() : val);
|
|
5
|
+
}
|
|
1
6
|
import { signVoucher } from './voucher.js';
|
|
7
|
+
import { Ic402Error, classifyNetworkError, probeX402 as probeX402Url, createEvmClient, getEvmNonce, getFeeData, broadcastTransaction as broadcastTx, registerAgent as registerAgentFlow, } from './evm.js';
|
|
2
8
|
/**
|
|
3
9
|
* ic402 TypeScript client SDK.
|
|
4
10
|
*
|
|
@@ -7,7 +13,6 @@ import { signVoucher } from './voucher.js';
|
|
|
7
13
|
*/
|
|
8
14
|
export class Ic402Client {
|
|
9
15
|
config;
|
|
10
|
-
totalSpent = 0n;
|
|
11
16
|
constructor(config) {
|
|
12
17
|
this.config = config;
|
|
13
18
|
}
|
|
@@ -16,13 +21,9 @@ export class Ic402Client {
|
|
|
16
21
|
*
|
|
17
22
|
* Flow: call method → if #paymentRequired → icrc2_approve → create sig → retry
|
|
18
23
|
*/
|
|
19
|
-
async call(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (!actorFactory) {
|
|
23
|
-
throw new Error('actorFactory required: provide a function that creates an actor for the canister');
|
|
24
|
-
}
|
|
25
|
-
const actor = actorFactory(canisterId);
|
|
24
|
+
async call(method, args, canisterId) {
|
|
25
|
+
const cid = canisterId ?? this.config.canisterId;
|
|
26
|
+
const actor = this.config.actorFactory(cid);
|
|
26
27
|
const result = await actor[method](...args);
|
|
27
28
|
// Check for payment required response
|
|
28
29
|
if (result && typeof result === 'object' && 'paymentRequired' in result) {
|
|
@@ -30,23 +31,14 @@ export class Ic402Client {
|
|
|
30
31
|
throw new Error('Payment required but autoPayment is disabled');
|
|
31
32
|
}
|
|
32
33
|
const requirement = result.paymentRequired;
|
|
33
|
-
// Check budget limits
|
|
34
|
-
if (this.config.budget?.maxPerRequest &&
|
|
35
|
-
requirement.amount > this.config.budget.maxPerRequest) {
|
|
36
|
-
throw new Error(`Amount ${requirement.amount} exceeds maxPerRequest ${this.config.budget.maxPerRequest}`);
|
|
37
|
-
}
|
|
38
|
-
if (this.config.budget?.maxTotal &&
|
|
39
|
-
this.totalSpent + requirement.amount > this.config.budget.maxTotal) {
|
|
40
|
-
throw new Error('Total budget exceeded');
|
|
41
|
-
}
|
|
42
34
|
if (!this.config.ledger || !this.config.ledgerActorFactory) {
|
|
43
35
|
throw new Error('Auto-approval requires ledger and ledgerActorFactory in config');
|
|
44
36
|
}
|
|
45
|
-
// ICRC-2 approve: allow the target canister to spend
|
|
37
|
+
// ICRC-2 approve: allow the target canister to spend amount + fee buffer
|
|
46
38
|
const ledgerActor = this.config.ledgerActorFactory(this.config.ledger);
|
|
47
39
|
const approveResult = await ledgerActor.icrc2_approve({
|
|
48
|
-
spender: { owner:
|
|
49
|
-
amount: requirement.amount,
|
|
40
|
+
spender: { owner: Principal.fromText(cid), subaccount: [] },
|
|
41
|
+
amount: requirement.amount + (this.config.approvalFeeBuffer ?? 100000n),
|
|
50
42
|
fee: [],
|
|
51
43
|
memo: [],
|
|
52
44
|
from_subaccount: [],
|
|
@@ -55,16 +47,16 @@ export class Ic402Client {
|
|
|
55
47
|
expires_at: [],
|
|
56
48
|
});
|
|
57
49
|
if (approveResult && typeof approveResult === 'object' && 'Err' in approveResult) {
|
|
58
|
-
throw new Error(`ICRC-2 approve failed: ${
|
|
50
|
+
throw new Error(`ICRC-2 approve failed: ${safeStringify(approveResult.Err)}`);
|
|
59
51
|
}
|
|
60
|
-
this.totalSpent += requirement.amount;
|
|
61
52
|
// Construct PaymentSignature from the requirement's nonce and retry
|
|
53
|
+
const sender = this.config.identity?.getPrincipal().toText() ?? '';
|
|
62
54
|
const sig = {
|
|
63
55
|
scheme: 'exact',
|
|
64
56
|
network: this.config.network,
|
|
65
57
|
signature: requirement.nonce,
|
|
66
58
|
publicKey: [],
|
|
67
|
-
sender
|
|
59
|
+
sender,
|
|
68
60
|
nonce: requirement.nonce,
|
|
69
61
|
authorization: [],
|
|
70
62
|
};
|
|
@@ -87,13 +79,10 @@ export class Ic402Client {
|
|
|
87
79
|
*
|
|
88
80
|
* Flow: requestSession → calculate deposit → icrc2_approve → openSession → SessionHandle
|
|
89
81
|
*/
|
|
90
|
-
async openSession(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
throw new Error('actorFactory required');
|
|
95
|
-
}
|
|
96
|
-
const actor = actorFactory(canisterId);
|
|
82
|
+
async openSession(sessionConfig, signer, canisterId) {
|
|
83
|
+
const cid = canisterId ?? this.config.canisterId;
|
|
84
|
+
const config = sessionConfig;
|
|
85
|
+
const actor = this.config.actorFactory(cid);
|
|
97
86
|
let intent = await actor.requestSession();
|
|
98
87
|
// For EVM sessions, override the intent's network, token, and recipient
|
|
99
88
|
if (config?.evmNetwork) {
|
|
@@ -107,19 +96,14 @@ export class Ic402Client {
|
|
|
107
96
|
const maxDeposit = config?.maxDeposit ?? this.config.sessions?.maxDeposit ?? intent.suggestedDeposit;
|
|
108
97
|
const autoClose = config?.autoClose ?? this.config.sessions?.autoClose ?? true;
|
|
109
98
|
const idleTimeout = config?.idleTimeout ?? this.config.sessions?.idleTimeout;
|
|
110
|
-
// Check budget
|
|
111
|
-
if (this.config.budget?.maxSessionDeposit &&
|
|
112
|
-
maxDeposit > this.config.budget.maxSessionDeposit) {
|
|
113
|
-
throw new Error(`Deposit ${maxDeposit} exceeds maxSessionDeposit ${this.config.budget.maxSessionDeposit}`);
|
|
114
|
-
}
|
|
115
99
|
const isEvm = !!config?.evmTxHash;
|
|
116
100
|
const network = config?.evmNetwork ?? this.config.network;
|
|
117
101
|
// ICRC-2 approve deposit amount (ICP sessions only)
|
|
118
102
|
if (!isEvm && this.config.autoPayment && this.config.ledger && this.config.ledgerActorFactory) {
|
|
119
103
|
const ledgerActor = this.config.ledgerActorFactory(this.config.ledger);
|
|
120
104
|
const approveResult = await ledgerActor.icrc2_approve({
|
|
121
|
-
spender: { owner:
|
|
122
|
-
amount: maxDeposit,
|
|
105
|
+
spender: { owner: Principal.fromText(cid), subaccount: [] },
|
|
106
|
+
amount: maxDeposit + (this.config.approvalFeeBuffer ?? 100000n),
|
|
123
107
|
fee: [],
|
|
124
108
|
memo: [],
|
|
125
109
|
from_subaccount: [],
|
|
@@ -128,10 +112,10 @@ export class Ic402Client {
|
|
|
128
112
|
expires_at: [],
|
|
129
113
|
});
|
|
130
114
|
if (approveResult && typeof approveResult === 'object' && 'Err' in approveResult) {
|
|
131
|
-
throw new Error(`ICRC-2 approve failed: ${
|
|
115
|
+
throw new Error(`ICRC-2 approve failed: ${safeStringify(approveResult.Err)}`);
|
|
132
116
|
}
|
|
133
117
|
}
|
|
134
|
-
const
|
|
118
|
+
const openConfig = {
|
|
135
119
|
maxDeposit,
|
|
136
120
|
autoClose,
|
|
137
121
|
idleTimeout: idleTimeout ? [idleTimeout] : [],
|
|
@@ -144,18 +128,37 @@ export class Ic402Client {
|
|
|
144
128
|
if (signer) {
|
|
145
129
|
pubKey = new Uint8Array(await signer.getPublicKey());
|
|
146
130
|
}
|
|
131
|
+
const evmAuth = config?.authorization;
|
|
147
132
|
const sig = {
|
|
148
133
|
scheme: 'exact',
|
|
149
134
|
network,
|
|
150
135
|
signature: isEvm
|
|
151
136
|
? Array.from(new TextEncoder().encode(config.evmTxHash))
|
|
152
137
|
: new Uint8Array(0),
|
|
153
|
-
publicKey: [
|
|
138
|
+
publicKey: [pubKey],
|
|
154
139
|
sender: config?.evmSender ?? '',
|
|
155
140
|
nonce: new Uint8Array(32),
|
|
156
|
-
authorization:
|
|
141
|
+
authorization: evmAuth
|
|
142
|
+
? [
|
|
143
|
+
{
|
|
144
|
+
from: evmAuth.from,
|
|
145
|
+
to: evmAuth.to,
|
|
146
|
+
value: typeof evmAuth.value === 'bigint' ? evmAuth.value : BigInt(evmAuth.value),
|
|
147
|
+
validAfter: typeof evmAuth.validAfter === 'bigint'
|
|
148
|
+
? evmAuth.validAfter
|
|
149
|
+
: BigInt(evmAuth.validAfter),
|
|
150
|
+
validBefore: typeof evmAuth.validBefore === 'bigint'
|
|
151
|
+
? evmAuth.validBefore
|
|
152
|
+
: BigInt(evmAuth.validBefore),
|
|
153
|
+
nonce: evmAuth.nonce,
|
|
154
|
+
v: evmAuth.v,
|
|
155
|
+
r: evmAuth.r,
|
|
156
|
+
s: evmAuth.s,
|
|
157
|
+
},
|
|
158
|
+
]
|
|
159
|
+
: [],
|
|
157
160
|
};
|
|
158
|
-
const result = await actor.openSession(
|
|
161
|
+
const result = await actor.openSession(openConfig, sig);
|
|
159
162
|
if ('err' in result) {
|
|
160
163
|
throw new Error(`Failed to open session: ${result.err}`);
|
|
161
164
|
}
|
|
@@ -230,7 +233,7 @@ export class Ic402Client {
|
|
|
230
233
|
if ('ok' in closeResult) {
|
|
231
234
|
return closeResult.ok;
|
|
232
235
|
}
|
|
233
|
-
throw new Error(`Failed to close session: ${
|
|
236
|
+
throw new Error(`Failed to close session: ${safeStringify(closeResult)}`);
|
|
234
237
|
},
|
|
235
238
|
};
|
|
236
239
|
return handle;
|
|
@@ -294,6 +297,255 @@ export class Ic402Client {
|
|
|
294
297
|
}
|
|
295
298
|
throw new Error('Unknown delivery method');
|
|
296
299
|
}
|
|
300
|
+
// ── EVM Remote Signing ──
|
|
301
|
+
/**
|
|
302
|
+
* Fetch from an x402-gated URL. The full flow:
|
|
303
|
+
* 1. Client probes the URL → gets 402 with payment requirements
|
|
304
|
+
* 2. Client calls canister signX402Payment → gets signed EIP-3009 header
|
|
305
|
+
* 3. Client retries the URL with the signed X-Payment header
|
|
306
|
+
*
|
|
307
|
+
* @param url - The x402-gated URL to fetch
|
|
308
|
+
* @param actorFactory - Factory to create canister actor
|
|
309
|
+
*/
|
|
310
|
+
async fetchX402(url, options) {
|
|
311
|
+
const { init, chainId } = options ?? {};
|
|
312
|
+
const cid = chainId ?? this.tryExtractChainId();
|
|
313
|
+
if (!cid) {
|
|
314
|
+
return {
|
|
315
|
+
status: 'error',
|
|
316
|
+
error: new Ic402Error('config_error', 'EVM chain ID required: set config.network to "eip155:<chainId>" or pass chainId to fetchX402'),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const actor = this.config.actorFactory(this.config.canisterId);
|
|
320
|
+
// 1. Probe
|
|
321
|
+
const probeResult = await probeX402Url(url, cid, init);
|
|
322
|
+
if (probeResult.status === 'free')
|
|
323
|
+
return probeResult;
|
|
324
|
+
if (probeResult.status === 'error')
|
|
325
|
+
return probeResult;
|
|
326
|
+
const { paymentOption } = probeResult;
|
|
327
|
+
// 2. Sign via canister
|
|
328
|
+
let signed;
|
|
329
|
+
try {
|
|
330
|
+
// Extract chain ID from payment option's network (e.g., "eip155:8453" → 8453)
|
|
331
|
+
const optionChainId = parseInt(paymentOption.network.replace('eip155:', ''), 10) || cid;
|
|
332
|
+
const result = await actor.signX402Payment(optionChainId, paymentOption.asset, paymentOption.recipient, paymentOption.amount, paymentOption.tokenName, paymentOption.tokenVersion);
|
|
333
|
+
if ('err' in result) {
|
|
334
|
+
return { status: 'error', error: new Ic402Error('sign_failed', result.err) };
|
|
335
|
+
}
|
|
336
|
+
signed = result.ok;
|
|
337
|
+
}
|
|
338
|
+
catch (e) {
|
|
339
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
340
|
+
return { status: 'error', error: new Ic402Error('sign_failed', msg, e) };
|
|
341
|
+
}
|
|
342
|
+
// 3. Retry with payment header
|
|
343
|
+
let response;
|
|
344
|
+
try {
|
|
345
|
+
const headers = new Headers(init?.headers);
|
|
346
|
+
headers.set('X-Payment', signed.header);
|
|
347
|
+
headers.set('Payment-Signature', signed.header);
|
|
348
|
+
response = await fetch(url, { ...init, headers });
|
|
349
|
+
}
|
|
350
|
+
catch (e) {
|
|
351
|
+
return { status: 'error', error: classifyNetworkError(e) };
|
|
352
|
+
}
|
|
353
|
+
const body = await response.text();
|
|
354
|
+
if (response.ok)
|
|
355
|
+
return { status: 'ok', code: response.status, body, paidAmount: signed.paidAmount };
|
|
356
|
+
if (response.status === 402)
|
|
357
|
+
return { status: 'error', error: new Ic402Error('settlement_failed', body.slice(0, 200)) };
|
|
358
|
+
return {
|
|
359
|
+
status: 'error',
|
|
360
|
+
error: new Ic402Error('http_error', `HTTP ${response.status}: ${body.slice(0, 200)}`),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Register the canister as an agent on the ERC-8004 IdentityRegistry.
|
|
365
|
+
* Full flow: get chain state → canister signs → client broadcasts → poll receipt.
|
|
366
|
+
*
|
|
367
|
+
* @param actorFactory - Factory to create canister actor
|
|
368
|
+
* @param rpcUrl - Optional custom RPC URL for the target chain
|
|
369
|
+
*/
|
|
370
|
+
async registerAgent(rpcUrl, chainId) {
|
|
371
|
+
const cid = chainId ?? this.extractChainId();
|
|
372
|
+
const actor = this.config.actorFactory(this.config.canisterId);
|
|
373
|
+
const evmAddress = await actor.getEvmAddress();
|
|
374
|
+
const rpc = rpcUrl ?? this.config.evmRpcUrl;
|
|
375
|
+
const result = await registerAgentFlow(async (nonce, maxFee, priorityFee) => {
|
|
376
|
+
const r = await actor.signAgentRegistration(nonce, maxFee, priorityFee);
|
|
377
|
+
if ('err' in r)
|
|
378
|
+
throw new Ic402Error('sign_failed', r.err);
|
|
379
|
+
return r.ok;
|
|
380
|
+
}, evmAddress, cid, rpc);
|
|
381
|
+
if (result.tokenId != null) {
|
|
382
|
+
try {
|
|
383
|
+
await actor.setAgentId?.(result.tokenId);
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
/* optional */
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return { tokenId: result.tokenId, txHash: result.txHash };
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Sign and broadcast an ERC-20 transfer.
|
|
393
|
+
* Client fetches chain state, canister signs, client broadcasts.
|
|
394
|
+
*/
|
|
395
|
+
async sendErc20Transfer(tokenAddress, recipient, amount, rpcUrl) {
|
|
396
|
+
const chainId = this.extractChainId();
|
|
397
|
+
const actor = this.config.actorFactory(this.config.canisterId);
|
|
398
|
+
const evmAddress = await actor.getEvmAddress();
|
|
399
|
+
const rpc = rpcUrl ?? this.config.evmRpcUrl;
|
|
400
|
+
const client = createEvmClient(chainId, rpc);
|
|
401
|
+
const [nonce, fees] = await Promise.all([getEvmNonce(client, evmAddress), getFeeData(client)]);
|
|
402
|
+
const result = await actor.signErc20Transfer(chainId, tokenAddress, recipient, amount, nonce, fees.maxFeePerGas, fees.maxPriorityFeePerGas);
|
|
403
|
+
if ('err' in result)
|
|
404
|
+
throw new Ic402Error('sign_failed', result.err);
|
|
405
|
+
const txHash = await broadcastTx(client, result.ok.rawTx);
|
|
406
|
+
return { txHash };
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Sign and broadcast a native ETH transfer.
|
|
410
|
+
*/
|
|
411
|
+
async sendEthTransfer(recipient, amountWei, rpcUrl) {
|
|
412
|
+
const chainId = this.extractChainId();
|
|
413
|
+
const actor = this.config.actorFactory(this.config.canisterId);
|
|
414
|
+
const evmAddress = await actor.getEvmAddress();
|
|
415
|
+
const rpc = rpcUrl ?? this.config.evmRpcUrl;
|
|
416
|
+
const client = createEvmClient(chainId, rpc);
|
|
417
|
+
const [nonce, fees] = await Promise.all([getEvmNonce(client, evmAddress), getFeeData(client)]);
|
|
418
|
+
const result = await actor.signEthTransfer(chainId, recipient, amountWei, 21000, nonce, fees.maxFeePerGas, fees.maxPriorityFeePerGas);
|
|
419
|
+
if ('err' in result)
|
|
420
|
+
throw new Ic402Error('sign_failed', result.err);
|
|
421
|
+
const txHash = await broadcastTx(client, result.ok.rawTx);
|
|
422
|
+
return { txHash };
|
|
423
|
+
}
|
|
424
|
+
/** Extract numeric chain ID from CAIP-2 network string, or null if not EVM. */
|
|
425
|
+
tryExtractChainId() {
|
|
426
|
+
const match = this.config.network.match(/^eip155:(\d+)$/);
|
|
427
|
+
return match ? parseInt(match[1], 10) : null;
|
|
428
|
+
}
|
|
429
|
+
/** Extract chain ID or throw. */
|
|
430
|
+
extractChainId() {
|
|
431
|
+
const id = this.tryExtractChainId();
|
|
432
|
+
if (id == null)
|
|
433
|
+
throw new Ic402Error('config_error', `Expected EVM network (eip155:*), got: ${this.config.network}`);
|
|
434
|
+
return id;
|
|
435
|
+
}
|
|
436
|
+
// ── Service Marketplace ──
|
|
437
|
+
/**
|
|
438
|
+
* List available services from the canister.
|
|
439
|
+
*/
|
|
440
|
+
async listServices() {
|
|
441
|
+
const actor = this.config.actorFactory(this.config.canisterId);
|
|
442
|
+
return actor.listServices();
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Submit a paid service request. Handles x402 payment automatically.
|
|
446
|
+
* Returns the job ID for polling.
|
|
447
|
+
*/
|
|
448
|
+
async submitServiceRequest(serviceId, params) {
|
|
449
|
+
const actor = this.config.actorFactory(this.config.canisterId);
|
|
450
|
+
const result = await actor.submitServiceRequest(serviceId, Array.from(params), []);
|
|
451
|
+
if (result && typeof result === 'object' && 'paymentRequired' in result) {
|
|
452
|
+
if (!this.config.autoPayment) {
|
|
453
|
+
throw new Ic402Error('config_error', 'Payment required but autoPayment is disabled');
|
|
454
|
+
}
|
|
455
|
+
if (!this.config.ledger || !this.config.ledgerActorFactory) {
|
|
456
|
+
throw new Ic402Error('config_error', 'Auto-approval requires ledger and ledgerActorFactory');
|
|
457
|
+
}
|
|
458
|
+
const requirement = result.paymentRequired;
|
|
459
|
+
const amount = Array.isArray(requirement) && requirement.length > 0 ? requirement[0].amount : 0n;
|
|
460
|
+
// Approve amount + fee buffer (ICRC-2 transfer_from deducts fee from allowance)
|
|
461
|
+
const approveAmount = amount + (this.config.approvalFeeBuffer ?? 100000n);
|
|
462
|
+
const ledgerActor = this.config.ledgerActorFactory(this.config.ledger);
|
|
463
|
+
const approveResult = await ledgerActor.icrc2_approve({
|
|
464
|
+
spender: { owner: Principal.fromText(this.config.canisterId), subaccount: [] },
|
|
465
|
+
amount: approveAmount,
|
|
466
|
+
fee: [],
|
|
467
|
+
memo: [],
|
|
468
|
+
from_subaccount: [],
|
|
469
|
+
created_at_time: [],
|
|
470
|
+
expected_allowance: [],
|
|
471
|
+
expires_at: [],
|
|
472
|
+
});
|
|
473
|
+
if (approveResult && typeof approveResult === 'object' && 'Err' in approveResult) {
|
|
474
|
+
throw new Ic402Error('sign_failed', `ICRC-2 approve failed: ${safeStringify(approveResult.Err)}`);
|
|
475
|
+
}
|
|
476
|
+
// Convert nonce to proper array — Candid may decode vec nat8 as Uint8Array or indexed object
|
|
477
|
+
const rawNonce = requirement[0]?.nonce ?? new Uint8Array(32);
|
|
478
|
+
const nonce = rawNonce instanceof Uint8Array
|
|
479
|
+
? Array.from(rawNonce)
|
|
480
|
+
: Array.isArray(rawNonce)
|
|
481
|
+
? rawNonce
|
|
482
|
+
: Object.values(rawNonce);
|
|
483
|
+
// Sender must be the caller's principal — derive from identity if available
|
|
484
|
+
const sender = this.config.identity?.getPrincipal().toText() ?? '';
|
|
485
|
+
const sig = {
|
|
486
|
+
scheme: 'exact',
|
|
487
|
+
network: this.config.network,
|
|
488
|
+
signature: nonce,
|
|
489
|
+
publicKey: [],
|
|
490
|
+
sender,
|
|
491
|
+
nonce,
|
|
492
|
+
authorization: [],
|
|
493
|
+
};
|
|
494
|
+
const retryResult = await actor.submitServiceRequest(serviceId, Array.from(params), [sig]);
|
|
495
|
+
if (retryResult && typeof retryResult === 'object' && 'ok' in retryResult) {
|
|
496
|
+
return retryResult.ok;
|
|
497
|
+
}
|
|
498
|
+
if (retryResult && typeof retryResult === 'object' && 'error' in retryResult) {
|
|
499
|
+
throw new Ic402Error('sign_failed', retryResult.error);
|
|
500
|
+
}
|
|
501
|
+
throw new Ic402Error('unknown', `Unexpected result: ${safeStringify(retryResult)}`);
|
|
502
|
+
}
|
|
503
|
+
if (result && typeof result === 'object' && 'ok' in result) {
|
|
504
|
+
return result.ok;
|
|
505
|
+
}
|
|
506
|
+
if (result && typeof result === 'object' && 'error' in result) {
|
|
507
|
+
throw new Ic402Error('sign_failed', result.error);
|
|
508
|
+
}
|
|
509
|
+
throw new Ic402Error('unknown', `Unexpected result: ${safeStringify(result)}`);
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Poll for a job result until completed or max attempts reached.
|
|
513
|
+
* Returns the full job record when done.
|
|
514
|
+
*/
|
|
515
|
+
async pollJobResult(jobId, maxAttempts = 30, intervalMs = 2000) {
|
|
516
|
+
const actor = this.config.actorFactory(this.config.canisterId);
|
|
517
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
518
|
+
const job = await actor.getJob(jobId);
|
|
519
|
+
// Candid opt returns [job] or []
|
|
520
|
+
const j = Array.isArray(job) && job.length > 0 ? job[0] : job;
|
|
521
|
+
if (!j)
|
|
522
|
+
throw new Ic402Error('unknown', `Job not found: ${jobId}`);
|
|
523
|
+
const status = j.status;
|
|
524
|
+
if ('Settled' in status || 'Verified' in status || 'Submitted' in status) {
|
|
525
|
+
return j;
|
|
526
|
+
}
|
|
527
|
+
if ('Disputed' in status) {
|
|
528
|
+
throw new Ic402Error('sign_failed', `Job disputed: ${jobId}`);
|
|
529
|
+
}
|
|
530
|
+
if ('Expired' in status || 'Refunded' in status) {
|
|
531
|
+
throw new Ic402Error('not_confirmed', `Job expired or refunded: ${jobId}`);
|
|
532
|
+
}
|
|
533
|
+
if (i < maxAttempts - 1) {
|
|
534
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
throw new Ic402Error('not_confirmed', `Job ${jobId} not completed within ${maxAttempts} attempts`);
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Dispute a job result (for BuyerConfirm verification).
|
|
541
|
+
*/
|
|
542
|
+
async disputeJob(jobId, reason) {
|
|
543
|
+
const actor = this.config.actorFactory(this.config.canisterId);
|
|
544
|
+
const result = await actor.disputeJob(jobId, reason);
|
|
545
|
+
if (result && typeof result === 'object' && 'err' in result) {
|
|
546
|
+
throw new Ic402Error('unknown', result.err);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
297
549
|
/**
|
|
298
550
|
* Discover agents via ERC-8004 registries.
|
|
299
551
|
*
|