@ftptech/x402-canton-client 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 +64 -0
- package/dist/cip56-signer.d.ts +82 -0
- package/dist/cip56-signer.d.ts.map +1 -0
- package/dist/cip56-signer.js +183 -0
- package/dist/cip56-signer.js.map +1 -0
- package/dist/fetch.d.ts +51 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +120 -0
- package/dist/fetch.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/keyfile-signer.d.ts +185 -0
- package/dist/keyfile-signer.d.ts.map +1 -0
- package/dist/keyfile-signer.js +281 -0
- package/dist/keyfile-signer.js.map +1 -0
- package/dist/scheme.d.ts +61 -0
- package/dist/scheme.d.ts.map +1 -0
- package/dist/scheme.js +164 -0
- package/dist/scheme.js.map +1 -0
- package/dist/signer.d.ts +126 -0
- package/dist/signer.d.ts.map +1 -0
- package/dist/signer.js +15 -0
- package/dist/signer.js.map +1 -0
- package/package.json +52 -0
package/dist/fetch.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wrapFetchWithCantonPayment — automate the x402 v2 "detect 402 →
|
|
3
|
+
* pay → retry" flow against any Canton-aware facilitator.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
*
|
|
7
|
+
* const fetchWithPay = wrapFetchWithCantonPayment(globalThis.fetch, signer);
|
|
8
|
+
* const res = await fetchWithPay("https://api.example.com/data");
|
|
9
|
+
* const settle = readPaymentResponseHeader(res);
|
|
10
|
+
*
|
|
11
|
+
* Wire format: x402 v2 only. v1 fallback is tracked in BACKLOG.md
|
|
12
|
+
* and lands when an older facilitator demands it.
|
|
13
|
+
*
|
|
14
|
+
* Limitations:
|
|
15
|
+
* - The original request body must be replayable (e.g. JSON string,
|
|
16
|
+
* Uint8Array, FormData). Streaming bodies will fail on retry.
|
|
17
|
+
* - Idempotency: callers should treat the wrapped fetch as
|
|
18
|
+
* at-most-twice. If the second response is still 402 we throw —
|
|
19
|
+
* no infinite-loop retries.
|
|
20
|
+
*/
|
|
21
|
+
import { HEADER_PAYMENT_REQUIRED_V2, HEADER_PAYMENT_SIGNATURE_V2, HEADER_PAYMENT_RESPONSE_V2, encodeBase64Json, decodeBase64Json, } from "@ftptech/x402-canton-core";
|
|
22
|
+
import { ExactCantonScheme, SchemeMethodMismatchError } from "./scheme.js";
|
|
23
|
+
/** Which on-ledger transfer methods a given signer can actually produce. */
|
|
24
|
+
function signerMethods(signer) {
|
|
25
|
+
const m = new Set();
|
|
26
|
+
if (signer.signTransferCommand)
|
|
27
|
+
m.add("external-party-amulet-rules");
|
|
28
|
+
if (signer.signCip56Transfer)
|
|
29
|
+
m.add("cip56-transfer-factory");
|
|
30
|
+
return m;
|
|
31
|
+
}
|
|
32
|
+
export class X402PaymentError extends Error {
|
|
33
|
+
code;
|
|
34
|
+
constructor(message, code) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.name = "X402PaymentError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function wrapFetchWithCantonPayment(fetch, signer, options = {}) {
|
|
41
|
+
const scheme = new ExactCantonScheme(signer);
|
|
42
|
+
return async function fetchWithPayment(input, init) {
|
|
43
|
+
// If the caller is already attempting payment (has the signature
|
|
44
|
+
// header) just pass through — don't recurse.
|
|
45
|
+
const initHeaders = new Headers(init?.headers);
|
|
46
|
+
if (initHeaders.has(HEADER_PAYMENT_SIGNATURE_V2)) {
|
|
47
|
+
return fetch(input, init);
|
|
48
|
+
}
|
|
49
|
+
const first = await fetch(input, init);
|
|
50
|
+
if (first.status !== 402)
|
|
51
|
+
return first;
|
|
52
|
+
const requiredHeader = first.headers.get(HEADER_PAYMENT_REQUIRED_V2);
|
|
53
|
+
if (!requiredHeader) {
|
|
54
|
+
throw new X402PaymentError("402 response missing PAYMENT-REQUIRED header (v1 wire format not supported in client v0.1)", "MISSING_PAYMENT_REQUIRED_HEADER");
|
|
55
|
+
}
|
|
56
|
+
// The PAYMENT-REQUIRED header is server-controlled and untrusted:
|
|
57
|
+
// bad base64, non-JSON, or JSON missing `accepts[]` must surface as a
|
|
58
|
+
// discriminated X402PaymentError, not leak a raw SyntaxError/TypeError
|
|
59
|
+
// out of the wrapped fetch (the whole contract of this wrapper is that
|
|
60
|
+
// it only ever throws X402PaymentError).
|
|
61
|
+
let required;
|
|
62
|
+
try {
|
|
63
|
+
required = decodeBase64Json(requiredHeader);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new X402PaymentError("402 PAYMENT-REQUIRED header is not valid base64-encoded JSON", "MALFORMED_PAYMENT_REQUIRED");
|
|
67
|
+
}
|
|
68
|
+
if (!required || !Array.isArray(required.accepts)) {
|
|
69
|
+
throw new X402PaymentError("402 PAYMENT-REQUIRED payload missing a valid accepts[] array", "MALFORMED_PAYMENT_REQUIRED");
|
|
70
|
+
}
|
|
71
|
+
const candidates = required.accepts.filter((a) => {
|
|
72
|
+
if (a.scheme !== "exact-canton")
|
|
73
|
+
return false;
|
|
74
|
+
if (options.networkFilter && !options.networkFilter(a.network))
|
|
75
|
+
return false;
|
|
76
|
+
return true;
|
|
77
|
+
});
|
|
78
|
+
if (candidates.length === 0) {
|
|
79
|
+
throw new X402PaymentError("no exact-canton entry in 402 accepts[] (or networkFilter excluded all)", "NO_ACCEPTABLE_SCHEME");
|
|
80
|
+
}
|
|
81
|
+
// Narrow to entries whose transferMethod THIS signer can actually produce.
|
|
82
|
+
// A merchant may advertise both v1 (external-party-amulet-rules) and cip56;
|
|
83
|
+
// a CIP-56-only wallet (e.g. the agent-wallet relay signer) must pick the
|
|
84
|
+
// cip56 entry rather than blindly take accepts[0] and fail. If the merchant
|
|
85
|
+
// advertised exact-canton entries but NONE match this wallet, raise a clear,
|
|
86
|
+
// named mismatch error instead of a cryptic downstream failure (plan §4.1/3).
|
|
87
|
+
const methods = signerMethods(signer);
|
|
88
|
+
const compatible = candidates.filter((a) => methods.has(a.extra.transferMethod));
|
|
89
|
+
if (compatible.length === 0) {
|
|
90
|
+
const required_ = [
|
|
91
|
+
...new Set(candidates.map((a) => a.extra.transferMethod)),
|
|
92
|
+
].join(", ");
|
|
93
|
+
const supported = [...methods].join(", ") || "(none)";
|
|
94
|
+
throw new SchemeMethodMismatchError(required_, supported);
|
|
95
|
+
}
|
|
96
|
+
const chosen = options.selectRequirements?.(compatible) ?? compatible[0];
|
|
97
|
+
const envelope = await scheme.createPaymentPayload(chosen, required.resource);
|
|
98
|
+
const retryHeaders = new Headers(init?.headers);
|
|
99
|
+
retryHeaders.set(HEADER_PAYMENT_SIGNATURE_V2, encodeBase64Json(envelope));
|
|
100
|
+
const retryInit = { ...init, headers: retryHeaders };
|
|
101
|
+
const second = await fetch(input, retryInit);
|
|
102
|
+
if (second.status === 402) {
|
|
103
|
+
throw new X402PaymentError("payment retry still returned 402 — facilitator rejected the payment", "PAYMENT_REJECTED");
|
|
104
|
+
}
|
|
105
|
+
return second;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Decode the `PAYMENT-RESPONSE` header (x402 v2) attached to a
|
|
110
|
+
* response that came back through wrapFetchWithCantonPayment.
|
|
111
|
+
* Returns null when the header is absent (e.g. response unrelated to
|
|
112
|
+
* x402).
|
|
113
|
+
*/
|
|
114
|
+
export function readPaymentResponseHeader(response) {
|
|
115
|
+
const header = response.headers.get(HEADER_PAYMENT_RESPONSE_V2);
|
|
116
|
+
if (!header)
|
|
117
|
+
return null;
|
|
118
|
+
return decodeBase64Json(header);
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KeyfileSigner — production `CantonSigner` implementation backed by
|
|
3
|
+
* a local Ed25519 PEM key, the JSON Ledger API v2's interactive-
|
|
4
|
+
* submission flow, and the Scan API for nonce + DSO lookups.
|
|
5
|
+
*
|
|
6
|
+
* Flow per `signTransferCommand`:
|
|
7
|
+
*
|
|
8
|
+
* 1. Read sender's next-expected nonce from
|
|
9
|
+
* `ScanClient.getTransferCommandCounter(party)`. First payment
|
|
10
|
+
* from a new party falls back to nonce 0.
|
|
11
|
+
* 2. Prepare an ExerciseCommand for
|
|
12
|
+
* `ExternalPartyAmuletRules_CreateTransferCommand` with the
|
|
13
|
+
* caller-supplied receiver / delegate / amount / description and
|
|
14
|
+
* our just-resolved nonce. The `ExternalPartyAmuletRules`
|
|
15
|
+
* contract id + template id + createdEventBlob must be supplied
|
|
16
|
+
* via `deps.externalPartyAmuletRules` (caller fetches it from
|
|
17
|
+
* Scan once at startup or via `loadExternalPartyAmuletRules`).
|
|
18
|
+
* 3. `CantonExternalPartySigner.prepareSignAndExecute` lands the
|
|
19
|
+
* transaction. The execute endpoint returns `updateId` but NOT
|
|
20
|
+
* events.
|
|
21
|
+
* 4. Query ACS via `CantonClient.queryActiveContracts` and filter
|
|
22
|
+
* to TransferCommands matching `(sender, nonce)`. The
|
|
23
|
+
* `TransferCommandCounter` ensures (sender, nonce) is unique,
|
|
24
|
+
* so the match is the cid we just created.
|
|
25
|
+
* 5. Return `{transferCommandCid, payerParty, nonce}`.
|
|
26
|
+
*
|
|
27
|
+
* Open question (logged in CRON-RUNBOOK.md):
|
|
28
|
+
* - The exact choice argument shape for
|
|
29
|
+
* `ExternalPartyAmuletRules_CreateTransferCommand` is inferred
|
|
30
|
+
* from the Splice source comment ("sender, receiver, delegate,
|
|
31
|
+
* amount, expiresAt, nonce, description, expectedDso"). Confirm
|
|
32
|
+
* against a live participant; adjust if the participant rejects.
|
|
33
|
+
*/
|
|
34
|
+
import { CantonClient, CantonExternalPartySigner, ScanClient, type ExternalPartyKey, type ScanFlavor } from "@ftptech/x402-canton-ledger";
|
|
35
|
+
import type { CantonSigner, SignedTransferCommand, SignTransferCommandInput } from "./signer.js";
|
|
36
|
+
export interface ExternalPartyAmuletRulesRef {
|
|
37
|
+
contractId: string;
|
|
38
|
+
/**
|
|
39
|
+
* RESOLVED package-id form of the ExternalPartyAmuletRules template, taken
|
|
40
|
+
* verbatim from Scan's `template_id`
|
|
41
|
+
* (e.g. `<pkgHash>:Splice.ExternalPartyAmuletRules:ExternalPartyAmuletRules`).
|
|
42
|
+
*
|
|
43
|
+
* MUST NOT be the `#package-name` form: the create flows through
|
|
44
|
+
* interactive-submission/prepare, which cannot parse a leading `#`
|
|
45
|
+
* (the participant rejects it with "non expected character 0x23").
|
|
46
|
+
* Use `loadExternalPartyAmuletRules(scan)` to fetch a correct ref.
|
|
47
|
+
*
|
|
48
|
+
* This is the EPAR's OWN package (its creation version), used ONLY for the
|
|
49
|
+
* disclosed-contract entry — it must match `createdEventBlob`'s package or the
|
|
50
|
+
* participant rejects the prepare (DisclosedContract.template_id mismatch).
|
|
51
|
+
* The exercise itself targets `exerciseTemplateId`.
|
|
52
|
+
*/
|
|
53
|
+
templateId: string;
|
|
54
|
+
/** Base64-encoded `created_event_blob` from Scan. Required for the
|
|
55
|
+
* disclosedContracts entry. */
|
|
56
|
+
createdEventBlob: string;
|
|
57
|
+
/**
|
|
58
|
+
* Template id for the `ExternalPartyAmuletRules_CreateTransferCommand`
|
|
59
|
+
* EXERCISE: `<currentSpliceAmuletPkg>:Splice.ExternalPartyAmuletRules:ExternalPartyAmuletRules`,
|
|
60
|
+
* where the package is the CURRENT splice-amulet (what `#splice-amulet`
|
|
61
|
+
* resolves to), taken from the live AmuletRules contract.
|
|
62
|
+
*
|
|
63
|
+
* Separate from `templateId` because the EPAR singleton can sit on an OLDER
|
|
64
|
+
* package whose `CreateTransferCommand` lacks `description`/`expectedDso`,
|
|
65
|
+
* while the current package has them (verified live on mainnet). Canton
|
|
66
|
+
* smart-contract-upgrade reconciles the older disclosed contract with this
|
|
67
|
+
* newer exercise package. Must also be the resolved package-id form (no `#`).
|
|
68
|
+
*/
|
|
69
|
+
exerciseTemplateId: string;
|
|
70
|
+
}
|
|
71
|
+
export interface KeyfileSignerDeps {
|
|
72
|
+
/** Wraps interactive-submission/prepare + execute. */
|
|
73
|
+
signer: CantonExternalPartySigner;
|
|
74
|
+
/** For ACS lookup of the newly-created TransferCommand cid. */
|
|
75
|
+
client: CantonClient;
|
|
76
|
+
/** For TransferCommandCounter (nonce). */
|
|
77
|
+
scan: ScanClient;
|
|
78
|
+
/** The Ed25519 keypair this signer signs with. */
|
|
79
|
+
key: ExternalPartyKey;
|
|
80
|
+
/** The agent's Canton party id. */
|
|
81
|
+
party: string;
|
|
82
|
+
/** The ExternalPartyAmuletRules contract reference (disclosed). */
|
|
83
|
+
externalPartyAmuletRules: ExternalPartyAmuletRulesRef;
|
|
84
|
+
/** DSO party id for the `expectedDso` field of
|
|
85
|
+
* `ExternalPartyAmuletRules_CreateTransferCommand`. If omitted, it's
|
|
86
|
+
* derived from the per-call synchronizerId (`global-domain::<hash>` →
|
|
87
|
+
* `DSO::<hash>`). Set explicitly if your network uses a
|
|
88
|
+
* non-conventional DSO/synchronizer naming. */
|
|
89
|
+
dso?: string;
|
|
90
|
+
/** Ledger user id submitted in the JsCommands envelope. */
|
|
91
|
+
userId?: string;
|
|
92
|
+
/** Default TransferCommand expiry window in seconds. Default 60. */
|
|
93
|
+
defaultExpirySeconds?: number;
|
|
94
|
+
/** Post-execute ACS poll attempts. interactive-submission/execute is async
|
|
95
|
+
* (the create commits on the completion stream AFTER execute returns), so
|
|
96
|
+
* the created TransferCommand is not visible immediately. Default 20. */
|
|
97
|
+
acsLookupAttempts?: number;
|
|
98
|
+
/** Delay between ACS poll attempts, ms. Default 1000. */
|
|
99
|
+
acsLookupDelayMs?: number;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Derive the DSO party id from a Global Synchronizer id.
|
|
103
|
+
*
|
|
104
|
+
* On Canton the DSO party and the global synchronizer share the same
|
|
105
|
+
* namespace fingerprint; the synchronizer id is `global-domain::<hash>`
|
|
106
|
+
* and the DSO party is `DSO::<hash>`. Verified live on DevNet:
|
|
107
|
+
* synchronizer `global-domain::1220be58c…` ↔ dso `DSO::1220be58c…`
|
|
108
|
+
* (the EPAR + AmuletRules payloads both report this dso). Returns null
|
|
109
|
+
* if the input doesn't have the expected `global-domain::` prefix.
|
|
110
|
+
*/
|
|
111
|
+
export declare function deriveDsoFromSynchronizer(synchronizerId: string): string | null;
|
|
112
|
+
/**
|
|
113
|
+
* Fetch the live ExternalPartyAmuletRules + AmuletRules contracts from Scan and
|
|
114
|
+
* return a ready-to-use `ExternalPartyAmuletRulesRef`. `templateId` is the
|
|
115
|
+
* EPAR's own resolved package (for the disclosed contract); `exerciseTemplateId`
|
|
116
|
+
* is the CURRENT splice-amulet package (derived from AmuletRules) for the
|
|
117
|
+
* exercise. Both are the resolved package-id form — what
|
|
118
|
+
* interactive-submission/prepare requires, never `#package-name`. Call once at
|
|
119
|
+
* startup and pass the result as `KeyfileSignerDeps.externalPartyAmuletRules`.
|
|
120
|
+
*/
|
|
121
|
+
export declare function loadExternalPartyAmuletRules(scan: ScanClient): Promise<ExternalPartyAmuletRulesRef>;
|
|
122
|
+
export declare class KeyfileSigner implements CantonSigner {
|
|
123
|
+
private readonly deps;
|
|
124
|
+
readonly party: string;
|
|
125
|
+
constructor(deps: KeyfileSignerDeps);
|
|
126
|
+
signTransferCommand(input: SignTransferCommandInput): Promise<SignedTransferCommand>;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Load an Ed25519 private key from a PEM file and derive the matching
|
|
130
|
+
* public key + canonical fingerprint via
|
|
131
|
+
* `ed25519KeyFromNodeKeyPair`.
|
|
132
|
+
*
|
|
133
|
+
* Either `keyPath` or `keyPem` must be supplied.
|
|
134
|
+
*/
|
|
135
|
+
export declare function loadEd25519KeyFromPem(args: {
|
|
136
|
+
keyPath?: string;
|
|
137
|
+
keyPem?: string;
|
|
138
|
+
fingerprint?: string;
|
|
139
|
+
}): ExternalPartyKey;
|
|
140
|
+
export interface MakeKeyfileSignerConfig {
|
|
141
|
+
/** Path to an Ed25519 PEM file. Mutually exclusive with `keyPem`. */
|
|
142
|
+
keyPath?: string;
|
|
143
|
+
/** Ed25519 PEM string (e.g. from an env var). */
|
|
144
|
+
keyPem?: string;
|
|
145
|
+
/** Optional fingerprint override (when participant canonicalizes
|
|
146
|
+
* the public key differently than SPKI-DER-SHA256). */
|
|
147
|
+
fingerprint?: string;
|
|
148
|
+
/** Agent's Canton party id (sender of TransferCommands). */
|
|
149
|
+
party: string;
|
|
150
|
+
/** JSON Ledger API v2 base URL for the agent's participant. */
|
|
151
|
+
participantUrl: string;
|
|
152
|
+
/** Bearer JWT for the agent's participant. */
|
|
153
|
+
participantToken: string;
|
|
154
|
+
/** Daml package name used in `templateRef`. */
|
|
155
|
+
packageName?: string;
|
|
156
|
+
/** Scan API base URL. */
|
|
157
|
+
scanUrl: string;
|
|
158
|
+
/** Optional Scan bearer token. */
|
|
159
|
+
scanToken?: string;
|
|
160
|
+
/** Scan flavor — "validator" (validator-local proxy) or "sv"
|
|
161
|
+
* (public SV Scan). Default "validator". */
|
|
162
|
+
scanFlavor?: ScanFlavor;
|
|
163
|
+
/** ExternalPartyAmuletRules disclosed-contract reference (operator
|
|
164
|
+
* fetches once at startup from Scan). */
|
|
165
|
+
externalPartyAmuletRules: ExternalPartyAmuletRulesRef;
|
|
166
|
+
/** Optional explicit DSO party id for `expectedDso`. If omitted,
|
|
167
|
+
* derived from the synchronizerId (`global-domain::<hash>` →
|
|
168
|
+
* `DSO::<hash>`). */
|
|
169
|
+
dso?: string;
|
|
170
|
+
/** Optional ledger user id passed in JsCommands. Default
|
|
171
|
+
* "x402-agent". */
|
|
172
|
+
userId?: string;
|
|
173
|
+
/** Optional default TransferCommand expiry seconds. Default 60. */
|
|
174
|
+
defaultExpirySeconds?: number;
|
|
175
|
+
/** Optional HTTP timeout for both participant + Scan. */
|
|
176
|
+
timeoutMs?: number;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Convenience builder that wires up `CantonClient`, `ScanClient`,
|
|
180
|
+
* `CantonExternalPartySigner`, and `loadEd25519KeyFromPem` from a
|
|
181
|
+
* flat config object so callers don't have to construct each
|
|
182
|
+
* dependency manually.
|
|
183
|
+
*/
|
|
184
|
+
export declare function makeKeyfileSigner(config: MakeKeyfileSignerConfig): KeyfileSigner;
|
|
185
|
+
//# sourceMappingURL=keyfile-signer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyfile-signer.d.ts","sourceRoot":"","sources":["../src/keyfile-signer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KeyfileSigner — production `CantonSigner` implementation backed by
|
|
3
|
+
* a local Ed25519 PEM key, the JSON Ledger API v2's interactive-
|
|
4
|
+
* submission flow, and the Scan API for nonce + DSO lookups.
|
|
5
|
+
*
|
|
6
|
+
* Flow per `signTransferCommand`:
|
|
7
|
+
*
|
|
8
|
+
* 1. Read sender's next-expected nonce from
|
|
9
|
+
* `ScanClient.getTransferCommandCounter(party)`. First payment
|
|
10
|
+
* from a new party falls back to nonce 0.
|
|
11
|
+
* 2. Prepare an ExerciseCommand for
|
|
12
|
+
* `ExternalPartyAmuletRules_CreateTransferCommand` with the
|
|
13
|
+
* caller-supplied receiver / delegate / amount / description and
|
|
14
|
+
* our just-resolved nonce. The `ExternalPartyAmuletRules`
|
|
15
|
+
* contract id + template id + createdEventBlob must be supplied
|
|
16
|
+
* via `deps.externalPartyAmuletRules` (caller fetches it from
|
|
17
|
+
* Scan once at startup or via `loadExternalPartyAmuletRules`).
|
|
18
|
+
* 3. `CantonExternalPartySigner.prepareSignAndExecute` lands the
|
|
19
|
+
* transaction. The execute endpoint returns `updateId` but NOT
|
|
20
|
+
* events.
|
|
21
|
+
* 4. Query ACS via `CantonClient.queryActiveContracts` and filter
|
|
22
|
+
* to TransferCommands matching `(sender, nonce)`. The
|
|
23
|
+
* `TransferCommandCounter` ensures (sender, nonce) is unique,
|
|
24
|
+
* so the match is the cid we just created.
|
|
25
|
+
* 5. Return `{transferCommandCid, payerParty, nonce}`.
|
|
26
|
+
*
|
|
27
|
+
* Open question (logged in CRON-RUNBOOK.md):
|
|
28
|
+
* - The exact choice argument shape for
|
|
29
|
+
* `ExternalPartyAmuletRules_CreateTransferCommand` is inferred
|
|
30
|
+
* from the Splice source comment ("sender, receiver, delegate,
|
|
31
|
+
* amount, expiresAt, nonce, description, expectedDso"). Confirm
|
|
32
|
+
* against a live participant; adjust if the participant rejects.
|
|
33
|
+
*/
|
|
34
|
+
import { readFileSync } from "node:fs";
|
|
35
|
+
import { createPrivateKey, createPublicKey, } from "node:crypto";
|
|
36
|
+
import { CantonClient, CantonExternalPartySigner, ScanClient, ed25519KeyFromNodeKeyPair, } from "@ftptech/x402-canton-ledger";
|
|
37
|
+
const TRANSFER_COMMAND_TEMPLATE_SUFFIX = ":Splice.ExternalPartyAmuletRules:TransferCommand";
|
|
38
|
+
const CREATE_TRANSFER_COMMAND_CHOICE = "ExternalPartyAmuletRules_CreateTransferCommand";
|
|
39
|
+
/**
|
|
40
|
+
* Derive the DSO party id from a Global Synchronizer id.
|
|
41
|
+
*
|
|
42
|
+
* On Canton the DSO party and the global synchronizer share the same
|
|
43
|
+
* namespace fingerprint; the synchronizer id is `global-domain::<hash>`
|
|
44
|
+
* and the DSO party is `DSO::<hash>`. Verified live on DevNet:
|
|
45
|
+
* synchronizer `global-domain::1220be58c…` ↔ dso `DSO::1220be58c…`
|
|
46
|
+
* (the EPAR + AmuletRules payloads both report this dso). Returns null
|
|
47
|
+
* if the input doesn't have the expected `global-domain::` prefix.
|
|
48
|
+
*/
|
|
49
|
+
export function deriveDsoFromSynchronizer(synchronizerId) {
|
|
50
|
+
const prefix = "global-domain::";
|
|
51
|
+
if (!synchronizerId.startsWith(prefix))
|
|
52
|
+
return null;
|
|
53
|
+
return `DSO::${synchronizerId.slice(prefix.length)}`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Fetch the live ExternalPartyAmuletRules + AmuletRules contracts from Scan and
|
|
57
|
+
* return a ready-to-use `ExternalPartyAmuletRulesRef`. `templateId` is the
|
|
58
|
+
* EPAR's own resolved package (for the disclosed contract); `exerciseTemplateId`
|
|
59
|
+
* is the CURRENT splice-amulet package (derived from AmuletRules) for the
|
|
60
|
+
* exercise. Both are the resolved package-id form — what
|
|
61
|
+
* interactive-submission/prepare requires, never `#package-name`. Call once at
|
|
62
|
+
* startup and pass the result as `KeyfileSignerDeps.externalPartyAmuletRules`.
|
|
63
|
+
*/
|
|
64
|
+
export async function loadExternalPartyAmuletRules(scan) {
|
|
65
|
+
const [eparRes, amuletRulesRes] = await Promise.all([
|
|
66
|
+
scan.getExternalPartyAmuletRules(),
|
|
67
|
+
scan.getAmuletRules(),
|
|
68
|
+
]);
|
|
69
|
+
const c = eparRes.external_party_amulet_rules.contract;
|
|
70
|
+
// The EXERCISE must target the CURRENT splice-amulet package (what
|
|
71
|
+
// `#splice-amulet` resolves to), which carries the full CreateTransferCommand
|
|
72
|
+
// signature (description/expectedDso). The EPAR singleton itself may sit on an
|
|
73
|
+
// older package, so derive the current package from the live AmuletRules
|
|
74
|
+
// contract (same splice-amulet DAR — both modules ship together).
|
|
75
|
+
const currentPkg = amuletRulesRes.amulet_rules.contract.template_id.split(":")[0];
|
|
76
|
+
return {
|
|
77
|
+
contractId: c.contract_id,
|
|
78
|
+
templateId: c.template_id,
|
|
79
|
+
createdEventBlob: c.created_event_blob,
|
|
80
|
+
exerciseTemplateId: `${currentPkg}:Splice.ExternalPartyAmuletRules:ExternalPartyAmuletRules`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export class KeyfileSigner {
|
|
84
|
+
deps;
|
|
85
|
+
party;
|
|
86
|
+
constructor(deps) {
|
|
87
|
+
this.deps = deps;
|
|
88
|
+
this.party = deps.party;
|
|
89
|
+
}
|
|
90
|
+
async signTransferCommand(input) {
|
|
91
|
+
// The create flows through interactive-submission/prepare, which cannot
|
|
92
|
+
// parse the `#package-name` templateId form (participant rejects it with
|
|
93
|
+
// "non expected character 0x23"). submit-and-wait tolerates `#name`, so a
|
|
94
|
+
// local-party smoke would not surface this. Fail fast before any I/O.
|
|
95
|
+
const eparRef = this.deps.externalPartyAmuletRules;
|
|
96
|
+
for (const [field, value] of [
|
|
97
|
+
["templateId", eparRef.templateId],
|
|
98
|
+
["exerciseTemplateId", eparRef.exerciseTemplateId],
|
|
99
|
+
]) {
|
|
100
|
+
if (value.startsWith("#")) {
|
|
101
|
+
throw new Error(`KeyfileSigner: externalPartyAmuletRules.${field} must be the ` +
|
|
102
|
+
'resolved package-id form (from Scan), not the "#package-name" ' +
|
|
103
|
+
"form — interactive-submission/prepare cannot parse a leading `#`. " +
|
|
104
|
+
`Use loadExternalPartyAmuletRules(scan). Got: ${value}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// 1. Resolve nonce.
|
|
108
|
+
let nextNonce = 0n;
|
|
109
|
+
try {
|
|
110
|
+
const counter = await this.deps.scan.getTransferCommandCounter(this.party);
|
|
111
|
+
nextNonce = BigInt(counter.transfer_command_counter.contract.payload.nextNonce);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// First payment from this party — counter does not exist yet.
|
|
115
|
+
}
|
|
116
|
+
// 2. Build choice argument.
|
|
117
|
+
const defaultExpirySeconds = this.deps.defaultExpirySeconds ?? 60;
|
|
118
|
+
const expiresAtMs = input.expiresAtMs ?? Date.now() + defaultExpirySeconds * 1000;
|
|
119
|
+
const epar = this.deps.externalPartyAmuletRules;
|
|
120
|
+
// `ExternalPartyAmuletRules_CreateTransferCommand` REQUIRES
|
|
121
|
+
// `expectedDso` (the DSO party id) — the participant rejects the
|
|
122
|
+
// prepare otherwise. Prefer an explicitly configured dso; else derive
|
|
123
|
+
// it from the synchronizer id, which on Canton is the same namespace
|
|
124
|
+
// with a `DSO` prefix instead of `global-domain` (verified live on
|
|
125
|
+
// DevNet: synchronizer `global-domain::1220<hash>` ↔ dso
|
|
126
|
+
// `DSO::1220<hash>`, and the cn-quickstart reference flow does the
|
|
127
|
+
// same swap). Fail loudly if neither is available rather than send an
|
|
128
|
+
// argument the ledger will reject.
|
|
129
|
+
const expectedDso = this.deps.dso ?? deriveDsoFromSynchronizer(input.synchronizerId);
|
|
130
|
+
if (!expectedDso) {
|
|
131
|
+
throw new Error("KeyfileSigner: cannot determine expectedDso — pass deps.dso, or a " +
|
|
132
|
+
`synchronizerId of the form "global-domain::<hash>" (got "${input.synchronizerId}")`);
|
|
133
|
+
}
|
|
134
|
+
const choiceArgument = {
|
|
135
|
+
sender: this.party,
|
|
136
|
+
receiver: input.receiver,
|
|
137
|
+
delegate: input.delegate,
|
|
138
|
+
amount: input.amount,
|
|
139
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
140
|
+
nonce: nextNonce.toString(),
|
|
141
|
+
description: input.description,
|
|
142
|
+
expectedDso,
|
|
143
|
+
};
|
|
144
|
+
// 3. Prepare → sign → execute.
|
|
145
|
+
const userId = this.deps.userId ?? "x402-agent";
|
|
146
|
+
const commandId = `xfer-cmd-${Date.now()}-${Math.random()
|
|
147
|
+
.toString(36)
|
|
148
|
+
.slice(2, 10)}`;
|
|
149
|
+
await this.deps.signer.prepareSignAndExecute({
|
|
150
|
+
userId,
|
|
151
|
+
commandId,
|
|
152
|
+
actAs: [this.party],
|
|
153
|
+
synchronizerId: input.synchronizerId,
|
|
154
|
+
disclosedContracts: [
|
|
155
|
+
{
|
|
156
|
+
templateId: epar.templateId,
|
|
157
|
+
contractId: epar.contractId,
|
|
158
|
+
createdEventBlob: epar.createdEventBlob,
|
|
159
|
+
synchronizerId: input.synchronizerId,
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
commands: [
|
|
163
|
+
{
|
|
164
|
+
ExerciseCommand: {
|
|
165
|
+
templateId: epar.exerciseTemplateId,
|
|
166
|
+
contractId: epar.contractId,
|
|
167
|
+
choice: CREATE_TRANSFER_COMMAND_CHOICE,
|
|
168
|
+
choiceArgument,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
}, this.deps.key);
|
|
173
|
+
// 4. Look up the created TransferCommand cid via ACS, matching
|
|
174
|
+
// (sender, nonce). interactive-submission/execute is ASYNC — it returns
|
|
175
|
+
// before the transaction commits (the commit lands on the completion
|
|
176
|
+
// stream), so poll the ACS until the contract appears. The
|
|
177
|
+
// TransferCommandCounter enforces nonce monotonicity per sender, so the
|
|
178
|
+
// (sender, nonce) match is unique.
|
|
179
|
+
const nonceStr = nextNonce.toString();
|
|
180
|
+
const attempts = this.deps.acsLookupAttempts ?? 20;
|
|
181
|
+
const delayMs = this.deps.acsLookupDelayMs ?? 1000;
|
|
182
|
+
let foundCid;
|
|
183
|
+
for (let attempt = 0; attempt < attempts && !foundCid; attempt++) {
|
|
184
|
+
if (attempt > 0)
|
|
185
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
186
|
+
const events = await this.deps.client.queryActiveContracts({
|
|
187
|
+
filtersByParty: {
|
|
188
|
+
[this.party]: {
|
|
189
|
+
cumulative: [
|
|
190
|
+
{
|
|
191
|
+
identifierFilter: {
|
|
192
|
+
WildcardFilter: {
|
|
193
|
+
value: { includeCreatedEventBlob: false },
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
for (const e of events) {
|
|
202
|
+
if (!e.templateId.endsWith(TRANSFER_COMMAND_TEMPLATE_SUFFIX))
|
|
203
|
+
continue;
|
|
204
|
+
const p = e.createArgument;
|
|
205
|
+
if (p.sender === this.party && p.nonce === nonceStr) {
|
|
206
|
+
foundCid = e.contractId;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (!foundCid) {
|
|
212
|
+
throw new Error(`KeyfileSigner: created TransferCommand not found in ACS after execute (party=${this.party}, nonce=${nonceStr}, polled ${attempts}x)`);
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
transferCommandCid: foundCid,
|
|
216
|
+
payerParty: this.party,
|
|
217
|
+
nonce: Number(nextNonce),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Load an Ed25519 private key from a PEM file and derive the matching
|
|
223
|
+
* public key + canonical fingerprint via
|
|
224
|
+
* `ed25519KeyFromNodeKeyPair`.
|
|
225
|
+
*
|
|
226
|
+
* Either `keyPath` or `keyPem` must be supplied.
|
|
227
|
+
*/
|
|
228
|
+
export function loadEd25519KeyFromPem(args) {
|
|
229
|
+
const pem = args.keyPem ??
|
|
230
|
+
(args.keyPath ? readFileSync(args.keyPath, "utf8") : null);
|
|
231
|
+
if (!pem) {
|
|
232
|
+
throw new Error("loadEd25519KeyFromPem: provide keyPath or keyPem");
|
|
233
|
+
}
|
|
234
|
+
const privateKey = createPrivateKey({ key: pem, format: "pem" });
|
|
235
|
+
const publicKey = createPublicKey(privateKey);
|
|
236
|
+
return ed25519KeyFromNodeKeyPair({
|
|
237
|
+
privateKey,
|
|
238
|
+
publicKey,
|
|
239
|
+
...(args.fingerprint !== undefined ? { fingerprint: args.fingerprint } : {}),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Convenience builder that wires up `CantonClient`, `ScanClient`,
|
|
244
|
+
* `CantonExternalPartySigner`, and `loadEd25519KeyFromPem` from a
|
|
245
|
+
* flat config object so callers don't have to construct each
|
|
246
|
+
* dependency manually.
|
|
247
|
+
*/
|
|
248
|
+
export function makeKeyfileSigner(config) {
|
|
249
|
+
const client = new CantonClient({
|
|
250
|
+
participantUrl: config.participantUrl,
|
|
251
|
+
token: config.participantToken,
|
|
252
|
+
packageName: config.packageName ?? "splice-amulet",
|
|
253
|
+
...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
|
|
254
|
+
});
|
|
255
|
+
const scan = new ScanClient({
|
|
256
|
+
scanUrl: config.scanUrl,
|
|
257
|
+
...(config.scanToken !== undefined ? { token: config.scanToken } : {}),
|
|
258
|
+
...(config.scanFlavor !== undefined ? { flavor: config.scanFlavor } : {}),
|
|
259
|
+
...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
|
|
260
|
+
});
|
|
261
|
+
const signer = new CantonExternalPartySigner(client);
|
|
262
|
+
const key = loadEd25519KeyFromPem({
|
|
263
|
+
...(config.keyPath !== undefined ? { keyPath: config.keyPath } : {}),
|
|
264
|
+
...(config.keyPem !== undefined ? { keyPem: config.keyPem } : {}),
|
|
265
|
+
...(config.fingerprint !== undefined ? { fingerprint: config.fingerprint } : {}),
|
|
266
|
+
});
|
|
267
|
+
return new KeyfileSigner({
|
|
268
|
+
signer,
|
|
269
|
+
client,
|
|
270
|
+
scan,
|
|
271
|
+
key,
|
|
272
|
+
party: config.party,
|
|
273
|
+
externalPartyAmuletRules: config.externalPartyAmuletRules,
|
|
274
|
+
...(config.dso !== undefined ? { dso: config.dso } : {}),
|
|
275
|
+
...(config.userId !== undefined ? { userId: config.userId } : {}),
|
|
276
|
+
...(config.defaultExpirySeconds !== undefined
|
|
277
|
+
? { defaultExpirySeconds: config.defaultExpirySeconds }
|
|
278
|
+
: {}),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
//# sourceMappingURL=keyfile-signer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyfile-signer.js","sourceRoot":"","sources":["../src/keyfile-signer.ts"],"names":[],"mappings":"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"}
|