@ftptech/canton-agent-wallet 0.1.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/LICENSE +201 -0
- package/README.md +134 -0
- package/dist/canton-hash.d.ts +61 -0
- package/dist/canton-hash.d.ts.map +1 -0
- package/dist/canton-hash.js +108 -0
- package/dist/canton-hash.js.map +1 -0
- package/dist/cli-args.d.ts +31 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +56 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +123 -0
- package/dist/cli.js.map +1 -0
- package/dist/hash-binding.d.ts +40 -0
- package/dist/hash-binding.d.ts.map +1 -0
- package/dist/hash-binding.js +20 -0
- package/dist/hash-binding.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +26 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +38 -0
- package/dist/keys.js.map +1 -0
- package/dist/onboard.d.ts +12 -0
- package/dist/onboard.d.ts.map +1 -0
- package/dist/onboard.js +152 -0
- package/dist/onboard.js.map +1 -0
- package/dist/pay.d.ts +16 -0
- package/dist/pay.d.ts.map +1 -0
- package/dist/pay.js +19 -0
- package/dist/pay.js.map +1 -0
- package/dist/relay-client.d.ts +128 -0
- package/dist/relay-client.d.ts.map +1 -0
- package/dist/relay-client.js +67 -0
- package/dist/relay-client.js.map +1 -0
- package/dist/relay-signer.d.ts +33 -0
- package/dist/relay-signer.d.ts.map +1 -0
- package/dist/relay-signer.js +44 -0
- package/dist/relay-signer.js.map +1 -0
- package/dist/store.d.ts +15 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +33 -0
- package/dist/store.js.map +1 -0
- package/dist/trusted-dso.d.ts +33 -0
- package/dist/trusted-dso.d.ts.map +1 -0
- package/dist/trusted-dso.js +36 -0
- package/dist/trusted-dso.js.map +1 -0
- package/dist/tx.d.ts +102 -0
- package/dist/tx.d.ts.map +1 -0
- package/dist/tx.js +328 -0
- package/dist/tx.js.map +1 -0
- package/dist/verify-prepared.d.ts +361 -0
- package/dist/verify-prepared.d.ts.map +1 -0
- package/dist/verify-prepared.js +2235 -0
- package/dist/verify-prepared.js.map +1 -0
- package/dist/withdraw.d.ts +18 -0
- package/dist/withdraw.d.ts.map +1 -0
- package/dist/withdraw.js +31 -0
- package/dist/withdraw.js.map +1 -0
- package/package.json +33 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* canton-agent-wallet CLI — the surface an agent (or its human) drives.
|
|
4
|
+
*
|
|
5
|
+
* create [--relay-url <url>] generate + onboard a self-custody wallet (idempotent)
|
|
6
|
+
* address print the party id (fund this)
|
|
7
|
+
* balance print CC balance
|
|
8
|
+
* claim accept incoming transfers (e.g. the initial funding)
|
|
9
|
+
* pay [--relay-url <url>] <url> fetch a URL, auto-paying any x402 402 challenge
|
|
10
|
+
* withdraw --to <p> send CC back out (--amount <cc> for a partial amount)
|
|
11
|
+
* export print the private key (backup; guard it)
|
|
12
|
+
*
|
|
13
|
+
* The relay (facilitator) URL is REQUIRED for the commands that talk to a
|
|
14
|
+
* relay (`create`, `pay`). There is intentionally no built-in default — a
|
|
15
|
+
* wrong/stale default would silently send an agent's payments to a dead host.
|
|
16
|
+
* Supply it via `--relay-url <url>` or the `CANTON_AGENT_RELAY_URL` env var.
|
|
17
|
+
* The current FTP facilitator is `http://46.225.91.251:4022` (plain HTTP, by
|
|
18
|
+
* IP — a TLS domain is planned but not yet live). `address`, `balance`,
|
|
19
|
+
* `claim`, `withdraw` and `export` reuse the relay stored in the wallet at
|
|
20
|
+
* `create` time and do not need the flag.
|
|
21
|
+
*
|
|
22
|
+
* Config via env: CANTON_AGENT_RELAY_URL, CANTON_AGENT_NETWORK,
|
|
23
|
+
* CANTON_AGENT_API_KEY. Wallet lives at ~/.canton-agent/wallet.json (0600).
|
|
24
|
+
*/
|
|
25
|
+
/* eslint-disable no-console -- CLI entrypoint: stdout is its result channel */
|
|
26
|
+
import { ensureWallet } from "./onboard.js";
|
|
27
|
+
import { loadWallet } from "./store.js";
|
|
28
|
+
import { makePayingFetch } from "./pay.js";
|
|
29
|
+
import { RelayClient } from "./relay-client.js";
|
|
30
|
+
import { claimAll } from "./tx.js";
|
|
31
|
+
import { withdraw } from "./withdraw.js";
|
|
32
|
+
import { flag, positionals, resolveNetwork, resolveRelayUrl, MISSING_RELAY_HELP, } from "./cli-args.js";
|
|
33
|
+
const API_KEY = process.env.CANTON_AGENT_API_KEY;
|
|
34
|
+
function fail(msg) {
|
|
35
|
+
console.error(msg);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
/** Resolve the relay URL or fail fast (never returns undefined). */
|
|
39
|
+
function requireRelay(args) {
|
|
40
|
+
return resolveRelayUrl(args) ?? fail(MISSING_RELAY_HELP);
|
|
41
|
+
}
|
|
42
|
+
async function main() {
|
|
43
|
+
const [cmd, ...args] = process.argv.slice(2);
|
|
44
|
+
switch (cmd) {
|
|
45
|
+
case "create": {
|
|
46
|
+
const relayUrl = requireRelay(args);
|
|
47
|
+
const w = await ensureWallet({
|
|
48
|
+
relayUrl,
|
|
49
|
+
network: resolveNetwork(args),
|
|
50
|
+
apiKey: API_KEY,
|
|
51
|
+
});
|
|
52
|
+
console.log(`wallet ready (${w.network})\n` +
|
|
53
|
+
` party: ${w.party}\n` +
|
|
54
|
+
` fund me: send CC to that party id, then run: canton-agent-wallet claim\n` +
|
|
55
|
+
` keyfile: ~/.canton-agent/wallet.json <- this IS your wallet; back it up`);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case "address": {
|
|
59
|
+
const w = loadWallet();
|
|
60
|
+
if (!w)
|
|
61
|
+
return fail("no wallet yet — run: canton-agent-wallet create");
|
|
62
|
+
console.log(w.party);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
case "balance": {
|
|
66
|
+
const w = loadWallet();
|
|
67
|
+
if (!w)
|
|
68
|
+
return fail("no wallet yet — run: canton-agent-wallet create");
|
|
69
|
+
const b = await new RelayClient({ relayUrl: w.relayUrl, apiKey: API_KEY }).balance(w.party);
|
|
70
|
+
console.log(`${b.cc} CC (${b.amulet} holding${b.amulet === 1 ? "" : "s"})`);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case "claim": {
|
|
74
|
+
const w = loadWallet();
|
|
75
|
+
if (!w)
|
|
76
|
+
return fail("no wallet yet — run: canton-agent-wallet create");
|
|
77
|
+
const r = await claimAll(new RelayClient({ relayUrl: w.relayUrl, apiKey: API_KEY }), w);
|
|
78
|
+
console.log(r.claimed ? `claimed ${r.claimed} incoming transfer(s)` : "nothing to claim");
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case "pay": {
|
|
82
|
+
const url = positionals(args)[0];
|
|
83
|
+
if (!url)
|
|
84
|
+
return fail("usage: canton-agent-wallet pay [--relay-url <url>] <url>");
|
|
85
|
+
const relayUrl = requireRelay(args);
|
|
86
|
+
const f = await makePayingFetch({
|
|
87
|
+
relayUrl,
|
|
88
|
+
network: resolveNetwork(args),
|
|
89
|
+
apiKey: API_KEY,
|
|
90
|
+
});
|
|
91
|
+
const res = await f(url);
|
|
92
|
+
console.log(`${res.status} ${res.statusText}`);
|
|
93
|
+
console.log(await res.text());
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case "withdraw": {
|
|
97
|
+
const to = flag(args, "--to");
|
|
98
|
+
if (!to)
|
|
99
|
+
return fail("usage: canton-agent-wallet withdraw --to <party> [--amount <cc>]");
|
|
100
|
+
const amount = flag(args, "--amount");
|
|
101
|
+
const r = await withdraw({ to, ...(amount ? { amount } : {}), apiKey: API_KEY });
|
|
102
|
+
console.log(`withdrew ${r.amount} CC to ${to}\n updateId: ${r.updateId}`);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case "export": {
|
|
106
|
+
const w = loadWallet();
|
|
107
|
+
if (!w)
|
|
108
|
+
return fail("no wallet yet — run: canton-agent-wallet create");
|
|
109
|
+
console.error("! PRIVATE KEY below — anyone who has it controls your funds. Store it securely.\n");
|
|
110
|
+
console.log(w.privateKeyPkcs8Pem);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
default:
|
|
114
|
+
console.error("canton-agent-wallet: create | address | balance | claim | pay <url> | withdraw --to <party> | export\n" +
|
|
115
|
+
" relay required for create/pay: --relay-url <url> or CANTON_AGENT_RELAY_URL");
|
|
116
|
+
process.exit(cmd ? 1 : 0);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
main().catch((e) => {
|
|
120
|
+
console.error("error:", e instanceof Error ? e.message : String(e));
|
|
121
|
+
process.exit(1);
|
|
122
|
+
});
|
|
123
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve how the agent binds the relay-returned prepared-transaction hash to
|
|
3
|
+
* the bytes it validated, for the CLI / autopay paths (`makeRelaySigner`,
|
|
4
|
+
* `withdraw`, `claimAll`).
|
|
5
|
+
*
|
|
6
|
+
* SECURITY DEFAULT: REAL cryptographic binding. With no configuration, the
|
|
7
|
+
* default now recomputes the prepared-transaction hash from the bytes the agent
|
|
8
|
+
* validated, EXACTLY as the Canton participant does on `execute`
|
|
9
|
+
* (HASHING_SCHEME_VERSION_V2 via `@canton-network/core-tx-visualizer`), and
|
|
10
|
+
* `assertHashBinding` signs the RECOMPUTED value only if it equals the relay
|
|
11
|
+
* `hash` — refusing otherwise. This satisfies the Canton Ledger API requirement:
|
|
12
|
+
* "clients MUST recompute the hash from the raw transaction if the preparing
|
|
13
|
+
* participant is not trusted." The previous fail-closed `{}` default existed
|
|
14
|
+
* only because no conformant recompute was wired; it now is.
|
|
15
|
+
*
|
|
16
|
+
* Escape hatch, off by default:
|
|
17
|
+
* CANTON_AGENT_TRUST_RELAY_HASH=1 accepts the relay's hash WITHOUT
|
|
18
|
+
* recomputation. This re-opens the blind-signing risk and is only acceptable
|
|
19
|
+
* when a human reviews each transfer or the relay is fully trusted. It is the
|
|
20
|
+
* documented last-resort fallback (e.g. if a future participant hashing-scheme
|
|
21
|
+
* change outpaces the pinned library and the operator must sign before the
|
|
22
|
+
* library catches up). Never assumed; must be explicitly set.
|
|
23
|
+
*
|
|
24
|
+
* The recompute is conformance-tested against captured live participant vectors
|
|
25
|
+
* (`canton-hash.conformance.test.ts`); a wrong recompute fails honest transfers
|
|
26
|
+
* CLOSED (refused) rather than mis-binding — it can never silently accept a
|
|
27
|
+
* relay-chosen hash.
|
|
28
|
+
*/
|
|
29
|
+
import type { HashBindingOptions } from "./verify-prepared.js";
|
|
30
|
+
/** The env var an operator sets to accept the relay hash without recomputing. */
|
|
31
|
+
export declare const TRUST_RELAY_HASH_ENV = "CANTON_AGENT_TRUST_RELAY_HASH";
|
|
32
|
+
/**
|
|
33
|
+
* Build the default `HashBindingOptions` for the CLI/autopay paths from the
|
|
34
|
+
* environment. Returns `{ trustRelayHash: true }` ONLY when the operator has
|
|
35
|
+
* explicitly opted into the dangerous escape hatch; otherwise returns the REAL
|
|
36
|
+
* binding `{ recomputeHash }` (the conformant V2 recompute), which makes
|
|
37
|
+
* `assertHashBinding` sign the recomputed hash and refuse on any mismatch.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveHashBinding(env?: NodeJS.ProcessEnv): HashBindingOptions;
|
|
40
|
+
//# sourceMappingURL=hash-binding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash-binding.d.ts","sourceRoot":"","sources":["../src/hash-binding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAG/D,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,kCAAkC,CAAC;AAOpE;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,kBAAkB,CAGpB"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { recomputeHash } from "./canton-hash.js";
|
|
2
|
+
/** The env var an operator sets to accept the relay hash without recomputing. */
|
|
3
|
+
export const TRUST_RELAY_HASH_ENV = "CANTON_AGENT_TRUST_RELAY_HASH";
|
|
4
|
+
function envSaysTrustRelayHash(env) {
|
|
5
|
+
const v = (env[TRUST_RELAY_HASH_ENV] ?? "").trim().toLowerCase();
|
|
6
|
+
return v === "1" || v === "true" || v === "yes";
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Build the default `HashBindingOptions` for the CLI/autopay paths from the
|
|
10
|
+
* environment. Returns `{ trustRelayHash: true }` ONLY when the operator has
|
|
11
|
+
* explicitly opted into the dangerous escape hatch; otherwise returns the REAL
|
|
12
|
+
* binding `{ recomputeHash }` (the conformant V2 recompute), which makes
|
|
13
|
+
* `assertHashBinding` sign the recomputed hash and refuse on any mismatch.
|
|
14
|
+
*/
|
|
15
|
+
export function resolveHashBinding(env = process.env) {
|
|
16
|
+
if (envSaysTrustRelayHash(env))
|
|
17
|
+
return { trustRelayHash: true };
|
|
18
|
+
return { recomputeHash };
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=hash-binding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash-binding.js","sourceRoot":"","sources":["../src/hash-binding.ts"],"names":[],"mappings":"AA6BA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,iFAAiF;AACjF,MAAM,CAAC,MAAM,oBAAoB,GAAG,+BAA+B,CAAC;AAEpE,SAAS,qBAAqB,CAAC,GAAsB;IACnD,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,oBAAoB,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACjE,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,KAAK,CAAC;AAClD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAChC,MAAyB,OAAO,CAAC,GAAG;IAEpC,IAAI,qBAAqB,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC;IAChE,OAAO,EAAE,aAAa,EAAE,CAAC;AAC3B,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from "./keys.js";
|
|
2
|
+
export * from "./store.js";
|
|
3
|
+
export * from "./relay-client.js";
|
|
4
|
+
export * from "./onboard.js";
|
|
5
|
+
export * from "./relay-signer.js";
|
|
6
|
+
export * from "./tx.js";
|
|
7
|
+
export * from "./withdraw.js";
|
|
8
|
+
export * from "./pay.js";
|
|
9
|
+
export * from "./hash-binding.js";
|
|
10
|
+
export * from "./canton-hash.js";
|
|
11
|
+
export * from "./trusted-dso.js";
|
|
12
|
+
export { assertPreparedTransferMatches, assertPreparedCreateTransferCommandMatches, assertHashBinding, assertOnboardingTopologyBindsKey, decodePrepared, extractTransfer, extractCreateTransferCommand, PreparedDecodeError, PreparedTransferMismatchError, PreparedHashUnavailableError, OnboardingTopologyMismatchError, type HashBindingOptions, type PreparedTransferExpectation, type PreparedCreateTransferCommandExpectation, type OnboardingTopologyExpectation, } from "./verify-prepared.js";
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,OAAO,EACL,6BAA6B,EAC7B,0CAA0C,EAC1C,iBAAiB,EACjB,gCAAgC,EAChC,cAAc,EACd,eAAe,EACf,4BAA4B,EAC5B,mBAAmB,EACnB,6BAA6B,EAC7B,4BAA4B,EAC5B,+BAA+B,EAC/B,KAAK,kBAAkB,EACvB,KAAK,2BAA2B,EAChC,KAAK,wCAAwC,EAC7C,KAAK,6BAA6B,GACnC,MAAM,sBAAsB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from "./keys.js";
|
|
2
|
+
export * from "./store.js";
|
|
3
|
+
export * from "./relay-client.js";
|
|
4
|
+
export * from "./onboard.js";
|
|
5
|
+
export * from "./relay-signer.js";
|
|
6
|
+
export * from "./tx.js";
|
|
7
|
+
export * from "./withdraw.js";
|
|
8
|
+
export * from "./pay.js";
|
|
9
|
+
export * from "./hash-binding.js";
|
|
10
|
+
export * from "./canton-hash.js";
|
|
11
|
+
export * from "./trusted-dso.js";
|
|
12
|
+
export { assertPreparedTransferMatches, assertPreparedCreateTransferCommandMatches, assertHashBinding, assertOnboardingTopologyBindsKey, decodePrepared, extractTransfer, extractCreateTransferCommand, PreparedDecodeError, PreparedTransferMismatchError, PreparedHashUnavailableError, OnboardingTopologyMismatchError, } from "./verify-prepared.js";
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,OAAO,EACL,6BAA6B,EAC7B,0CAA0C,EAC1C,iBAAiB,EACjB,gCAAgC,EAChC,cAAc,EACd,eAAe,EACf,4BAA4B,EAC5B,mBAAmB,EACnB,6BAA6B,EAC7B,4BAA4B,EAC5B,+BAA+B,GAKhC,MAAM,sBAAsB,CAAC"}
|
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ed25519 key material for a self-custody agent wallet.
|
|
3
|
+
*
|
|
4
|
+
* The agent generates and holds this key. The participant only ever sees the
|
|
5
|
+
* SPKI/DER public key (during onboarding); the private key never leaves the
|
|
6
|
+
* agent. Serialized as strings so it can live in ~/.canton-agent/wallet.json.
|
|
7
|
+
*/
|
|
8
|
+
import { type KeyObject } from "node:crypto";
|
|
9
|
+
export interface AgentKeyMaterial {
|
|
10
|
+
/** Base64 of the SPKI/DER public key — what onboarding sends as keyData. */
|
|
11
|
+
publicKeySpkiB64: string;
|
|
12
|
+
/** PKCS8 PEM private key — self-custody secret, persisted locally only. */
|
|
13
|
+
privateKeyPkcs8Pem: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function generateAgentKey(): AgentKeyMaterial;
|
|
16
|
+
export declare function loadPrivateKey(pkcs8Pem: string): KeyObject;
|
|
17
|
+
export declare function loadPublicKey(spkiB64: string): KeyObject;
|
|
18
|
+
/**
|
|
19
|
+
* Sign a base64 hash (the `multiHash` from generate-topology, or a
|
|
20
|
+
* `preparedTransactionHash` from interactive-submission) with the agent's key.
|
|
21
|
+
* Returns a base64 64-byte R||S Ed25519 signature — `SIGNATURE_FORMAT_CONCAT`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function signHashB64(hashB64: string, pkcs8Pem: string): string;
|
|
24
|
+
/** Verify a signHashB64 result — used in tests + the sign-before-trust check. */
|
|
25
|
+
export declare function verifyHashB64(hashB64: string, signatureB64: string, spkiB64: string): boolean;
|
|
26
|
+
//# sourceMappingURL=keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAML,KAAK,SAAS,EACf,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,gBAAgB;IAC/B,4EAA4E;IAC5E,gBAAgB,EAAE,MAAM,CAAC;IACzB,2EAA2E;IAC3E,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,wBAAgB,gBAAgB,IAAI,gBAAgB,CAQnD;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAE1D;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAMxD;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMrE;AAED,iFAAiF;AACjF,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,GACd,OAAO,CAOT"}
|
package/dist/keys.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ed25519 key material for a self-custody agent wallet.
|
|
3
|
+
*
|
|
4
|
+
* The agent generates and holds this key. The participant only ever sees the
|
|
5
|
+
* SPKI/DER public key (during onboarding); the private key never leaves the
|
|
6
|
+
* agent. Serialized as strings so it can live in ~/.canton-agent/wallet.json.
|
|
7
|
+
*/
|
|
8
|
+
import { generateKeyPairSync, createPrivateKey, createPublicKey, sign as cryptoSign, verify as cryptoVerify, } from "node:crypto";
|
|
9
|
+
export function generateAgentKey() {
|
|
10
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
11
|
+
return {
|
|
12
|
+
publicKeySpkiB64: Buffer.from(publicKey.export({ type: "spki", format: "der" })).toString("base64"),
|
|
13
|
+
privateKeyPkcs8Pem: privateKey.export({ type: "pkcs8", format: "pem" }),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function loadPrivateKey(pkcs8Pem) {
|
|
17
|
+
return createPrivateKey(pkcs8Pem);
|
|
18
|
+
}
|
|
19
|
+
export function loadPublicKey(spkiB64) {
|
|
20
|
+
return createPublicKey({
|
|
21
|
+
key: Buffer.from(spkiB64, "base64"),
|
|
22
|
+
format: "der",
|
|
23
|
+
type: "spki",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Sign a base64 hash (the `multiHash` from generate-topology, or a
|
|
28
|
+
* `preparedTransactionHash` from interactive-submission) with the agent's key.
|
|
29
|
+
* Returns a base64 64-byte R||S Ed25519 signature — `SIGNATURE_FORMAT_CONCAT`.
|
|
30
|
+
*/
|
|
31
|
+
export function signHashB64(hashB64, pkcs8Pem) {
|
|
32
|
+
return cryptoSign(null, Buffer.from(hashB64, "base64"), createPrivateKey(pkcs8Pem)).toString("base64");
|
|
33
|
+
}
|
|
34
|
+
/** Verify a signHashB64 result — used in tests + the sign-before-trust check. */
|
|
35
|
+
export function verifyHashB64(hashB64, signatureB64, spkiB64) {
|
|
36
|
+
return cryptoVerify(null, Buffer.from(hashB64, "base64"), loadPublicKey(spkiB64), Buffer.from(signatureB64, "base64"));
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=keys.js.map
|
package/dist/keys.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,eAAe,EACf,IAAI,IAAI,UAAU,EAClB,MAAM,IAAI,YAAY,GAEvB,MAAM,aAAa,CAAC;AASrB,MAAM,UAAU,gBAAgB;IAC9B,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,mBAAmB,CAAC,SAAS,CAAC,CAAC;IACjE,OAAO;QACL,gBAAgB,EAAE,MAAM,CAAC,IAAI,CAC3B,SAAS,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAClD,CAAC,QAAQ,CAAC,QAAQ,CAAC;QACpB,kBAAkB,EAAE,UAAU,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAW;KAClF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,QAAgB;IAC7C,OAAO,gBAAgB,CAAC,QAAQ,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,OAAe;IAC3C,OAAO,eAAe,CAAC;QACrB,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC;QACnC,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,MAAM;KACb,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,QAAgB;IAC3D,OAAO,UAAU,CACf,IAAI,EACJ,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,EAC9B,gBAAgB,CAAC,QAAQ,CAAC,CAC3B,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AACvB,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,aAAa,CAC3B,OAAe,EACf,YAAoB,EACpB,OAAe;IAEf,OAAO,YAAY,CACjB,IAAI,EACJ,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,EAC9B,aAAa,CAAC,OAAO,CAAC,EACtB,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,CACpC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type AgentWallet } from "./store.js";
|
|
2
|
+
import { RelayClient } from "./relay-client.js";
|
|
3
|
+
export interface EnsureWalletOpts {
|
|
4
|
+
relayUrl: string;
|
|
5
|
+
network: string;
|
|
6
|
+
apiKey?: string | undefined;
|
|
7
|
+
partyHint?: string;
|
|
8
|
+
/** Injectable for tests; defaults to a real RelayClient. */
|
|
9
|
+
relay?: RelayClient;
|
|
10
|
+
}
|
|
11
|
+
export declare function ensureWallet(opts: EnsureWalletOpts): Promise<AgentWallet>;
|
|
12
|
+
//# sourceMappingURL=onboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"onboard.d.ts","sourceRoot":"","sources":["../src/onboard.ts"],"names":[],"mappings":"AAUA,OAAO,EAA0B,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAOhD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,WAAW,CAAC,CAyG/E"}
|
package/dist/onboard.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ensureWallet — lazy, idempotent self-custody onboarding.
|
|
3
|
+
*
|
|
4
|
+
* If a wallet already exists, return it (the agent reuses the same one forever).
|
|
5
|
+
* Otherwise generate a key, onboard the external party through the relay
|
|
6
|
+
* (generate-topology → sign the multiHash locally → allocate), persist, return.
|
|
7
|
+
* This is the flow proven by the Phase 0 live spike.
|
|
8
|
+
*/
|
|
9
|
+
import { ED25519_WIRE_CONSTANTS } from "@ftptech/x402-canton-ledger";
|
|
10
|
+
import { generateAgentKey, signHashB64 } from "./keys.js";
|
|
11
|
+
import { loadWallet, saveWallet } from "./store.js";
|
|
12
|
+
import { RelayClient } from "./relay-client.js";
|
|
13
|
+
import { assertOnboardingTopologyBindsKey, OnboardingTopologyMismatchError, } from "./verify-prepared.js";
|
|
14
|
+
import { recomputeTopologyMultiHash, fingerprintHex } from "./canton-hash.js";
|
|
15
|
+
export async function ensureWallet(opts) {
|
|
16
|
+
const existing = loadWallet();
|
|
17
|
+
if (existing)
|
|
18
|
+
return existing;
|
|
19
|
+
const key = generateAgentKey();
|
|
20
|
+
const relay = opts.relay ?? new RelayClient({ relayUrl: opts.relayUrl, apiKey: opts.apiKey });
|
|
21
|
+
const prep = await relay.onboardPrepare({
|
|
22
|
+
publicKey: {
|
|
23
|
+
format: ED25519_WIRE_CONSTANTS.publicKeyFormat,
|
|
24
|
+
keyData: key.publicKeySpkiB64,
|
|
25
|
+
keySpec: ED25519_WIRE_CONSTANTS.keySpec,
|
|
26
|
+
},
|
|
27
|
+
partyHint: opts.partyHint ?? "agent",
|
|
28
|
+
});
|
|
29
|
+
// VERIFY-BEFORE-SIGN (onboarding) — TWO complementary, fail-closed legs:
|
|
30
|
+
// (1) STRUCTURAL: decode the topology transactions and prove they onboard the
|
|
31
|
+
// agent's OWN key into the agent's OWN namespace (authoritative mappings
|
|
32
|
+
// bind exactly the agent's key, threshold 1, no foreign co-holders).
|
|
33
|
+
// (2) HASH RECOMPUTE: recompute the multiHash + fingerprint locally (below)
|
|
34
|
+
// and sign the RECOMPUTED value, so the signature is bound to the exact
|
|
35
|
+
// bytes we structurally verified.
|
|
36
|
+
// Either a relay that binds a different key/party (custody hijack, caught by 1)
|
|
37
|
+
// or one that pairs honest-looking topology with the hash of a different bundle
|
|
38
|
+
// (caught by 2) is refused. See verify-prepared.ts / canton-hash.ts.
|
|
39
|
+
assertOnboardingTopologyBindsKey(prep.onboardingTransactions, {
|
|
40
|
+
publicKeySpkiB64: key.publicKeySpkiB64,
|
|
41
|
+
party: prep.party,
|
|
42
|
+
publicKeyFingerprint: prep.publicKeyFingerprint,
|
|
43
|
+
});
|
|
44
|
+
// HASH RECOMPUTE (onboarding): bind the signed multiHash to the EXACT topology
|
|
45
|
+
// bytes we just structurally verified. The relay's `hashToSign` is the combined
|
|
46
|
+
// multiHash over the onboarding transactions; recompute it locally EXACTLY as
|
|
47
|
+
// the participant does on `allocate` (HashPurpose 11 per-tx, 55 combined; the
|
|
48
|
+
// official @canton-network/core-tx-visualizer primitives), compare, and sign the
|
|
49
|
+
// RECOMPUTED value — never the relay's. A relay that returns honest-looking
|
|
50
|
+
// topology paired with the multiHash of a DIFFERENT bundle is refused here.
|
|
51
|
+
const recomputedMultiHash = await recomputeTopologyMultiHash(prep.onboardingTransactions);
|
|
52
|
+
if (recomputedMultiHash !== prep.hashToSign) {
|
|
53
|
+
throw new OnboardingTopologyMismatchError(`relay-returned onboarding hashToSign does NOT match the multiHash recomputed ` +
|
|
54
|
+
`from the onboarding transactions — refusing to sign (possible tampered/` +
|
|
55
|
+
`compromised relay supplying the hash of a different topology bundle)`);
|
|
56
|
+
}
|
|
57
|
+
// LOCAL FINGERPRINT assert: the prior namespace check compared two
|
|
58
|
+
// relay-supplied strings (party namespace vs publicKeyFingerprint). Derive the
|
|
59
|
+
// fingerprint locally from the agent's OWN public-key bytes and assert it equals
|
|
60
|
+
// the relay's publicKeyFingerprint — closing the relay-vs-relay namespace trust
|
|
61
|
+
// gap (the namespace is the fingerprint; if the relay lies about the fingerprint
|
|
62
|
+
// it lies about the namespace). Fail-closed.
|
|
63
|
+
await assertLocalFingerprintMatches(key.publicKeySpkiB64, prep.publicKeyFingerprint);
|
|
64
|
+
const signature = {
|
|
65
|
+
format: ED25519_WIRE_CONSTANTS.signatureFormat,
|
|
66
|
+
// Sign the RECOMPUTED multiHash, not the relay's hashToSign (they are proven
|
|
67
|
+
// equal above; signing the recomputed value makes the binding explicit).
|
|
68
|
+
signature: signHashB64(recomputedMultiHash, key.privateKeyPkcs8Pem),
|
|
69
|
+
signingAlgorithmSpec: ED25519_WIRE_CONSTANTS.signingAlgorithmSpec,
|
|
70
|
+
signedBy: prep.publicKeyFingerprint,
|
|
71
|
+
};
|
|
72
|
+
const fin = await relay.onboardFinalize({
|
|
73
|
+
onboardingTransactions: prep.onboardingTransactions,
|
|
74
|
+
multiHashSignatures: [signature],
|
|
75
|
+
});
|
|
76
|
+
// The party we PERSIST (and will later act_as) must be the one we just proved
|
|
77
|
+
// lives in the agent key's namespace. The verified anchor is prep.party (it was
|
|
78
|
+
// checked against publicKeyFingerprint above); if the finalize step returns a
|
|
79
|
+
// different party, it must STILL be in that same verified namespace, else a
|
|
80
|
+
// relay could swap the persisted identity after the topology check. Fail-closed.
|
|
81
|
+
const party = fin.party || prep.party;
|
|
82
|
+
if (party !== prep.party) {
|
|
83
|
+
const sep = party.lastIndexOf("::");
|
|
84
|
+
const ns = sep > 0 ? party.slice(sep + 2) : "";
|
|
85
|
+
if (ns !== prep.publicKeyFingerprint) {
|
|
86
|
+
throw new OnboardingTopologyMismatchError(`onboard/finalize returned party ${JSON.stringify(party)} whose namespace does not match ` +
|
|
87
|
+
`the verified key fingerprint ${JSON.stringify(prep.publicKeyFingerprint)} — refusing to ` +
|
|
88
|
+
`persist a wallet for a party outside the agent key's own namespace`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const wallet = {
|
|
92
|
+
network: opts.network,
|
|
93
|
+
relayUrl: opts.relayUrl,
|
|
94
|
+
party,
|
|
95
|
+
publicKeySpkiB64: key.publicKeySpkiB64,
|
|
96
|
+
privateKeyPkcs8Pem: key.privateKeyPkcs8Pem,
|
|
97
|
+
publicKeyFingerprint: prep.publicKeyFingerprint,
|
|
98
|
+
createdAt: new Date().toISOString(),
|
|
99
|
+
};
|
|
100
|
+
saveWallet(wallet);
|
|
101
|
+
return wallet;
|
|
102
|
+
}
|
|
103
|
+
/** The bare 32-byte Ed25519 public-key point from an SPKI/DER base64 (the point
|
|
104
|
+
* is the trailing 32 bytes of the SPKI encoding). */
|
|
105
|
+
function rawEd25519Point(spkiB64) {
|
|
106
|
+
const der = Buffer.from(spkiB64, "base64");
|
|
107
|
+
return der.subarray(Math.max(0, der.length - 32));
|
|
108
|
+
}
|
|
109
|
+
/** Normalize a Canton fingerprint hex for comparison: lowercase; tolerate an
|
|
110
|
+
* optional `1220` (0x12 0x20 multihash sha2-256) prefix on either side so a
|
|
111
|
+
* framed-vs-unframed representation still compares equal. */
|
|
112
|
+
function normalizeFingerprint(fp) {
|
|
113
|
+
const lower = fp.trim().toLowerCase();
|
|
114
|
+
return lower.startsWith("1220") ? lower.slice(4) : lower;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Derive the public-key fingerprint LOCALLY from the agent's own key bytes and
|
|
118
|
+
* assert it equals the relay-returned `publicKeyFingerprint`. Fail-closed.
|
|
119
|
+
*
|
|
120
|
+
* WHICH KEY-BYTE FORM: Canton's fingerprint preimage is `HashPurpose 12 ||
|
|
121
|
+
* <key bytes>`, but whether `<key bytes>` is the SPKI/DER the agent sent or the
|
|
122
|
+
* bare 32-byte Ed25519 point is not pinned offline (spec B.5 flags this as
|
|
123
|
+
* needing a live `generate-topology` vector to confirm). We compute BOTH
|
|
124
|
+
* candidate fingerprints and accept iff the relay's value equals EITHER. This is
|
|
125
|
+
* still a real binding against the attack we care about — a relay returning the
|
|
126
|
+
* fingerprint of a FOREIGN key matches NEITHER candidate and is refused — while
|
|
127
|
+
* tolerating the spec ambiguity until a live vector pins the exact form. The
|
|
128
|
+
* conformance test (`canton-hash.conformance.test.ts`) records, once a live
|
|
129
|
+
* topology vector is dropped in, which form Canton actually uses.
|
|
130
|
+
*/
|
|
131
|
+
async function assertLocalFingerprintMatches(publicKeySpkiB64, relayFingerprint) {
|
|
132
|
+
if (typeof relayFingerprint !== "string" || relayFingerprint.length === 0) {
|
|
133
|
+
throw new OnboardingTopologyMismatchError("relay returned an empty publicKeyFingerprint — refusing to sign the onboarding " +
|
|
134
|
+
"topology (cannot bind the party namespace to the agent's key)");
|
|
135
|
+
}
|
|
136
|
+
const derBytes = Buffer.from(publicKeySpkiB64, "base64");
|
|
137
|
+
const pointBytes = rawEd25519Point(publicKeySpkiB64);
|
|
138
|
+
const [fpFromDer, fpFromPoint] = await Promise.all([
|
|
139
|
+
fingerprintHex(derBytes),
|
|
140
|
+
fingerprintHex(pointBytes),
|
|
141
|
+
]);
|
|
142
|
+
const want = normalizeFingerprint(relayFingerprint);
|
|
143
|
+
const candidates = [fpFromDer, fpFromPoint].map(normalizeFingerprint);
|
|
144
|
+
if (!candidates.includes(want)) {
|
|
145
|
+
throw new OnboardingTopologyMismatchError(`relay-returned publicKeyFingerprint ${JSON.stringify(relayFingerprint)} does not ` +
|
|
146
|
+
`equal the fingerprint derived locally from the agent's own public key ` +
|
|
147
|
+
`(neither the SPKI/DER nor the bare-point preimage matched) — refusing to sign ` +
|
|
148
|
+
`the onboarding topology (possible tampered/compromised relay claiming a foreign ` +
|
|
149
|
+
`key's fingerprint as the agent's namespace)`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=onboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"onboard.js","sourceRoot":"","sources":["../src/onboard.ts"],"names":[],"mappings":"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"}
|
package/dist/pay.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type EnsureWalletOpts } from "./onboard.js";
|
|
2
|
+
import type { HashBindingOptions } from "./verify-prepared.js";
|
|
3
|
+
export interface PayOpts extends EnsureWalletOpts {
|
|
4
|
+
/** @deprecated ignored; the relay sets the participant user. */
|
|
5
|
+
userId?: string;
|
|
6
|
+
/**
|
|
7
|
+
* How to bind the relay-returned hash to the validated bytes before signing
|
|
8
|
+
* each autopay transfer. Omitted → the env-resolved default, which is the REAL
|
|
9
|
+
* conformant V2 hash recompute (canton-hash.ts) unless CANTON_AGENT_TRUST_
|
|
10
|
+
* RELAY_HASH opts into the escape hatch. Supply a `recomputeHash` to override.
|
|
11
|
+
* See HashBindingOptions / verify-prepared.ts.
|
|
12
|
+
*/
|
|
13
|
+
hashBinding?: HashBindingOptions;
|
|
14
|
+
}
|
|
15
|
+
export declare function makePayingFetch(opts: PayOpts): Promise<typeof globalThis.fetch>;
|
|
16
|
+
//# sourceMappingURL=pay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pay.d.ts","sourceRoot":"","sources":["../src/pay.ts"],"names":[],"mappings":"AAQA,OAAO,EAAgB,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEnE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D,MAAM,WAAW,OAAQ,SAAQ,gBAAgB;IAC/C,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,kBAAkB,CAAC;CAClC;AAED,wBAAsB,eAAe,CACnC,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC,OAAO,UAAU,CAAC,KAAK,CAAC,CAOlC"}
|
package/dist/pay.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* makePayingFetch — a drop-in fetch that auto-pays x402 challenges from the
|
|
3
|
+
* agent's self-custody wallet. Lazily creates the wallet on first use, then
|
|
4
|
+
* wraps fetch so any 402 is paid (resolve factory via relay → build CIP-56
|
|
5
|
+
* transfer → sign locally → execute via relay) and the request retried. The
|
|
6
|
+
* agent just "fetches".
|
|
7
|
+
*/
|
|
8
|
+
import { wrapFetchWithCantonPayment } from "@ftptech/x402-canton-client";
|
|
9
|
+
import { ensureWallet } from "./onboard.js";
|
|
10
|
+
import { makeRelaySigner } from "./relay-signer.js";
|
|
11
|
+
export async function makePayingFetch(opts) {
|
|
12
|
+
const wallet = await ensureWallet(opts);
|
|
13
|
+
const signer = makeRelaySigner(wallet, {
|
|
14
|
+
apiKey: opts.apiKey,
|
|
15
|
+
...(opts.hashBinding ? { hashBinding: opts.hashBinding } : {}),
|
|
16
|
+
});
|
|
17
|
+
return wrapFetchWithCantonPayment(globalThis.fetch, signer);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=pay.js.map
|
package/dist/pay.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pay.js","sourceRoot":"","sources":["../src/pay.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AACzE,OAAO,EAAE,YAAY,EAAyB,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAgBpD,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAa;IAEb,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,EAAE;QACrC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC/D,CAAC,CAAC;IACH,OAAO,0BAA0B,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AAC9D,CAAC"}
|