@routedock/nulth-sdk 0.2.2 → 0.3.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/README.md +131 -12
- package/dist/index.cjs +210 -10
- package/dist/index.d.cts +62 -2
- package/dist/index.d.ts +62 -2
- package/dist/index.js +203 -10
- package/package.json +29 -2
package/README.md
CHANGED
|
@@ -2,16 +2,135 @@
|
|
|
2
2
|
|
|
3
3
|
**Status: STUB — not ready for production.**
|
|
4
4
|
|
|
5
|
-
This package provides an **insecure mock prover** (`insecureMockProof`)
|
|
6
|
-
SHA-512 digest as a placeholder
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
This package provides an **insecure mock prover** (`insecureMockProof`). It produces a
|
|
6
|
+
SHA-512 digest as a placeholder; no real zero-knowledge proof is generated. Use it
|
|
7
|
+
only for **testnet development**, never for custody or production policy enforcement.
|
|
8
|
+
`NulthClient` rejects every network except `testnet` and every prover except `mock`.
|
|
9
|
+
See [Issue #143](https://github.com/winsznx/routedock/issues/143) for production prover work.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
Node.js **22 or newer**, matching the supported runtime of the RouteDock packages.
|
|
14
|
+
TypeScript examples use ES2022 or later for native `bigint` support.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @routedock/nulth-sdk
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Example: initialize a testnet client
|
|
23
|
+
|
|
24
|
+
Set `NULTH_ACCOUNT` to your testnet smart-account contract address (`C...`) and
|
|
25
|
+
`TESTNET_PAYEE` to an allowed testnet recipient (`G...` or `C...`). This example
|
|
26
|
+
initializes local policy state and prints commitments; it does not submit a payment.
|
|
27
|
+
Persist the witness secret securely if you need to recreate the same commitments.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { randomBytes } from 'node:crypto'
|
|
31
|
+
import {
|
|
32
|
+
createPolicyState,
|
|
33
|
+
NulthClient,
|
|
34
|
+
stroopsToUsdc,
|
|
35
|
+
} from '@routedock/nulth-sdk'
|
|
36
|
+
|
|
37
|
+
const nulthAccount = process.env.NULTH_ACCOUNT
|
|
38
|
+
const payee = process.env.TESTNET_PAYEE
|
|
39
|
+
if (!nulthAccount || !payee) {
|
|
40
|
+
throw new Error('Set NULTH_ACCOUNT and TESTNET_PAYEE to testnet addresses')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const policy = createPolicyState({
|
|
44
|
+
dailyCapUsdc: '1.00',
|
|
45
|
+
allowedPayees: [payee],
|
|
46
|
+
witnessSecret: randomBytes(32).toString('hex'),
|
|
47
|
+
})
|
|
48
|
+
const client = new NulthClient({
|
|
49
|
+
nulthAccount,
|
|
50
|
+
network: 'testnet',
|
|
51
|
+
prover: 'mock',
|
|
52
|
+
policy,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
console.log(client.allowlistCommitment, client.capCommitment)
|
|
56
|
+
console.log(stroopsToUsdc(client.dailySpendStroops)) // '0'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Before using the signing adapter, supply the payment's real ledger sequence, asset,
|
|
60
|
+
payee, and amount through `setNulthPaymentContext`. `createNulthSigner` checks the
|
|
61
|
+
actual Soroban authorization preimage against that context. A missing context or
|
|
62
|
+
mismatched transfer is rejected. These client-side checks do not make the mock proof secure.
|
|
63
|
+
|
|
64
|
+
## Exports
|
|
65
|
+
|
|
66
|
+
| Export | Purpose |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `NulthClient` | Testnet-only client with policy checks, mock proof construction, encoding, and spend/commitment getters. |
|
|
69
|
+
| `createPolicyState` | Initialize off-chain policy from a decimal USDC cap, payees, witness secret, and optional ledger/expiry. |
|
|
70
|
+
| `createNulthSigner` | Create an x402-compatible signer for a Nulth account using the mock client. |
|
|
71
|
+
| `setNulthPaymentContext` | Set the next payment context on the configuration used by the signer. |
|
|
72
|
+
| `paymentContextFromManifest` | Extract asset, payee, amount, and ledger context from manifest pricing. |
|
|
73
|
+
| `assertAuthEntryMatchesContext` | Reject an authorization preimage whose transfer does not match the expected payer, payee, asset, or amount. |
|
|
74
|
+
| `usdcToStroops`, `stroopsToUsdc`, `USDC_DECIMALS` | Convert decimal USDC strings and integer units using seven decimal places. |
|
|
75
|
+
| `commitAllowlist`, `commitDailyCap` | Compute salted SHA-256 commitments to policy values. |
|
|
76
|
+
| `dayBucket` | Convert a ledger sequence to its 17,280-ledger day bucket. |
|
|
77
|
+
| `hashPayee`, `authDigestFromEntry`, `sha256Hex` | Hash payees, authorization preimages, and byte/string inputs. |
|
|
78
|
+
| `encodeAuthSignature`, `decodeAuthSignature` | Encode or decode the mock signature envelope as base64 JSON; decoding is not proof verification. |
|
|
79
|
+
| `insecureMockProof`, `DEFAULT_PROVER` | Produce deterministic SHA-512 placeholder bytes and identify the only supported backend (`mock`). |
|
|
80
|
+
| `NulthPolicyError` | Typed policy rejection with a `code` such as `daily_cap_exceeded` or `auth_entry_mismatch`. |
|
|
81
|
+
| `NulthPolicyState`, `NulthClientConfig`, `PaymentAuthContext` | Types for policy state, client configuration, and payment authorization inputs. |
|
|
82
|
+
| `NulthSignerConfig`, `NulthStellarSigner` | Types for mutable signer configuration and its signing interface. |
|
|
83
|
+
| `NulthProof`, `NulthAuthSignature`, `ProverBackend` | Types for the mock proof, signature envelope, and prover selection. |
|
|
84
|
+
|
|
85
|
+
## What the mock prover does and does not prove
|
|
86
|
+
|
|
87
|
+
`NulthClient` checks the local allowlist, daily cap, and optional session expiry,
|
|
88
|
+
then hashes public inputs into deterministic placeholder bytes. Building a proof
|
|
89
|
+
updates the client's in-memory daily spend; it is not evidence of on-chain settlement
|
|
90
|
+
and does not provide durable accounting across process restarts.
|
|
91
|
+
|
|
92
|
+
The digest is **not a zero-knowledge proof** and proves neither knowledge of a secret
|
|
93
|
+
witness nor satisfaction of the policy to an on-chain verifier. Anyone can reproduce
|
|
94
|
+
it from the public preimage. Commitments alone do not provide a production security
|
|
95
|
+
boundary, and the public inputs expose payment amounts, day buckets, and hashes.
|
|
96
|
+
A real prover/verifier integration is still required before this package can protect funds.
|
|
97
|
+
|
|
98
|
+
For contributors, `pnpm --filter @routedock/nulth-sdk check:readme` typechecks every
|
|
99
|
+
TypeScript example against the built package exports. It also runs during `pnpm verify`.
|
|
100
|
+
## Diagnostics and configuration errors
|
|
101
|
+
|
|
102
|
+
Diagnostics are silent by default. Pass a `logger(level, message, fields?)` in
|
|
103
|
+
`NulthClientConfig` (also accepted by `createNulthSigner`) to receive the mock-prover
|
|
104
|
+
warning. The sink is compatible with RouteDock's `RouteDockLogger`; the RouteDock
|
|
105
|
+
SDK adapter retains its console-backed default and accepts the same override.
|
|
106
|
+
|
|
107
|
+
Invalid networks and prover backends throw `NulthConfigError`, with code
|
|
108
|
+
`unsupported_network` or `unsupported_prover`. The RouteDock adapter maps these
|
|
109
|
+
to `RouteDockManifestError`; policy failures remain `NulthPolicyError`.
|
|
110
|
+
|
|
111
|
+
## Paying through x402 with a Nulth account
|
|
112
|
+
|
|
113
|
+
`src/scheme.ts` carries the pieces the SDK needs to let a Nulth ZK account sign an
|
|
114
|
+
x402 payment:
|
|
115
|
+
|
|
116
|
+
- `NULTH_SIGNER_MARKER` — a `Symbol.for` key set on every signer from
|
|
117
|
+
`createNulthSigner()`. `@routedock/routedock` uses it to pick the Nulth payment
|
|
118
|
+
scheme instead of the plain ed25519 one, so no caller has to opt in.
|
|
119
|
+
- `nulthProofScVal` / `decodeNulthProofScVal` — the `zk-v1` envelope, a
|
|
120
|
+
`scvVec` of one map with `nulth`, `version`, `proof`, `auth_digest`,
|
|
121
|
+
`allowlist_commitment`, `cap_commitment`, `payee_hash`, `amount_stroops` and
|
|
122
|
+
`day_bucket`. The proof and its public inputs go into
|
|
123
|
+
`SorobanAddressCredentials.signature`; the allowlist and the daily cap never
|
|
124
|
+
leave the agent.
|
|
125
|
+
- `nulthAuthorizeEntry` — the `authorizeEntry` replacement. stellar-sdk's
|
|
126
|
+
default writes `scvVec([nativeToScVal({public_key, signature})])`, and for a
|
|
127
|
+
`C...` contract account that path throws
|
|
128
|
+
`invalid version byte. expected 48, got 16` (see
|
|
129
|
+
[Issue #356](https://github.com/winsznx/routedock/issues/356)), so a Nulth
|
|
130
|
+
payment can never be signed with it.
|
|
131
|
+
|
|
132
|
+
**What still is not here:** the contract side. There is no Nulth account contract
|
|
133
|
+
in this repository that implements `__check_auth` against these public inputs, and
|
|
134
|
+
no Groth16 verifier is deployed, so a proof produced by the mock prover is not
|
|
135
|
+
settled on-chain. Testnet builds are for developing the client path only.
|
|
17
136
|
|
package/dist/index.cjs
CHANGED
|
@@ -21,7 +21,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
DEFAULT_PROVER: () => DEFAULT_PROVER,
|
|
24
|
+
NULTH_SIGNATURE_VERSION: () => NULTH_SIGNATURE_VERSION,
|
|
25
|
+
NULTH_SIGNER_MARKER: () => NULTH_SIGNER_MARKER,
|
|
24
26
|
NulthClient: () => NulthClient,
|
|
27
|
+
NulthConfigError: () => NulthConfigError,
|
|
25
28
|
NulthPolicyError: () => NulthPolicyError,
|
|
26
29
|
USDC_DECIMALS: () => USDC_DECIMALS,
|
|
27
30
|
assertAuthEntryMatchesContext: () => assertAuthEntryMatchesContext,
|
|
@@ -32,9 +35,13 @@ __export(index_exports, {
|
|
|
32
35
|
createPolicyState: () => createPolicyState,
|
|
33
36
|
dayBucket: () => dayBucket,
|
|
34
37
|
decodeAuthSignature: () => decodeAuthSignature,
|
|
38
|
+
decodeNulthProofScVal: () => decodeNulthProofScVal,
|
|
35
39
|
encodeAuthSignature: () => encodeAuthSignature,
|
|
36
40
|
hashPayee: () => hashPayee,
|
|
37
41
|
insecureMockProof: () => insecureMockProof,
|
|
42
|
+
isNulthStellarSigner: () => isNulthStellarSigner,
|
|
43
|
+
nulthAuthorizeEntry: () => nulthAuthorizeEntry,
|
|
44
|
+
nulthProofScVal: () => nulthProofScVal,
|
|
38
45
|
paymentContextFromManifest: () => paymentContextFromManifest,
|
|
39
46
|
setNulthPaymentContext: () => setNulthPaymentContext,
|
|
40
47
|
sha256Hex: () => sha256Hex,
|
|
@@ -44,6 +51,14 @@ __export(index_exports, {
|
|
|
44
51
|
module.exports = __toCommonJS(index_exports);
|
|
45
52
|
|
|
46
53
|
// src/types.ts
|
|
54
|
+
var NulthConfigError = class extends Error {
|
|
55
|
+
code;
|
|
56
|
+
constructor(code, message) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = "NulthConfigError";
|
|
59
|
+
this.code = code;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
47
62
|
var NulthPolicyError = class extends Error {
|
|
48
63
|
code;
|
|
49
64
|
constructor(code, message) {
|
|
@@ -54,9 +69,51 @@ var NulthPolicyError = class extends Error {
|
|
|
54
69
|
};
|
|
55
70
|
|
|
56
71
|
// src/proof.ts
|
|
57
|
-
var import_node_crypto = require("crypto");
|
|
58
72
|
var import_stellar_sdk = require("@stellar/stellar-sdk");
|
|
59
73
|
|
|
74
|
+
// src/encoding.ts
|
|
75
|
+
var import_sha2 = require("@noble/hashes/sha2.js");
|
|
76
|
+
var encoder = new TextEncoder();
|
|
77
|
+
var decoder = new TextDecoder();
|
|
78
|
+
function utf8ToBytes(text) {
|
|
79
|
+
return encoder.encode(text);
|
|
80
|
+
}
|
|
81
|
+
function bytesToUtf8(bytes) {
|
|
82
|
+
return decoder.decode(bytes);
|
|
83
|
+
}
|
|
84
|
+
function bytesToBase64(bytes) {
|
|
85
|
+
const chunk = 32768;
|
|
86
|
+
let binary = "";
|
|
87
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
88
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
89
|
+
}
|
|
90
|
+
return btoa(binary);
|
|
91
|
+
}
|
|
92
|
+
function base64ToBytes(b64) {
|
|
93
|
+
const end = b64.indexOf("=");
|
|
94
|
+
let clean = (end === -1 ? b64 : b64.slice(0, end)).replace(/-/g, "+").replace(/_/g, "/").replace(/[^A-Za-z0-9+/]/g, "");
|
|
95
|
+
if (clean.length % 4 === 1) clean = clean.slice(0, -1);
|
|
96
|
+
const binary = atob(clean);
|
|
97
|
+
const bytes = new Uint8Array(binary.length);
|
|
98
|
+
for (let i = 0; i < binary.length; i++) {
|
|
99
|
+
bytes[i] = binary.charCodeAt(i);
|
|
100
|
+
}
|
|
101
|
+
return bytes;
|
|
102
|
+
}
|
|
103
|
+
function bytesToHex(bytes) {
|
|
104
|
+
let out = "";
|
|
105
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
106
|
+
out += bytes[i].toString(16).padStart(2, "0");
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
function sha256Bytes(data) {
|
|
111
|
+
return (0, import_sha2.sha256)(typeof data === "string" ? utf8ToBytes(data) : data);
|
|
112
|
+
}
|
|
113
|
+
function sha512Bytes(data) {
|
|
114
|
+
return (0, import_sha2.sha512)(typeof data === "string" ? utf8ToBytes(data) : data);
|
|
115
|
+
}
|
|
116
|
+
|
|
60
117
|
// src/usdc.ts
|
|
61
118
|
var USDC_DECIMALS = 7;
|
|
62
119
|
var USDC_SCALE = 10n ** BigInt(USDC_DECIMALS);
|
|
@@ -92,8 +149,7 @@ function stroopsToUsdc(units) {
|
|
|
92
149
|
|
|
93
150
|
// src/proof.ts
|
|
94
151
|
function sha256Hex(data) {
|
|
95
|
-
|
|
96
|
-
return (0, import_node_crypto.createHash)("sha256").update(buf).digest("hex");
|
|
152
|
+
return bytesToHex(sha256Bytes(data));
|
|
97
153
|
}
|
|
98
154
|
function commitDailyCap(dailyCapStroops, witnessSecret) {
|
|
99
155
|
return sha256Hex(`${witnessSecret}:cap:${dailyCapStroops.toString()}`);
|
|
@@ -109,7 +165,7 @@ function dayBucket(ledgerSequence) {
|
|
|
109
165
|
return Math.floor(ledgerSequence / 17280);
|
|
110
166
|
}
|
|
111
167
|
function authDigestFromEntry(authEntryBase64) {
|
|
112
|
-
const entryHash = (0, import_stellar_sdk.hash)(
|
|
168
|
+
const entryHash = (0, import_stellar_sdk.hash)(base64ToBytes(authEntryBase64));
|
|
113
169
|
return sha256Hex(entryHash);
|
|
114
170
|
}
|
|
115
171
|
function assertAuthEntryMatchesContext(authEntry, from, ctx) {
|
|
@@ -175,17 +231,17 @@ function assertAuthEntryMatchesContext(authEntry, from, ctx) {
|
|
|
175
231
|
}
|
|
176
232
|
var DEFAULT_PROVER = "mock";
|
|
177
233
|
function insecureMockProof(preimage) {
|
|
178
|
-
return (
|
|
234
|
+
return bytesToBase64(sha512Bytes(`nulth:${preimage}`));
|
|
179
235
|
}
|
|
180
236
|
function encodeAuthSignature(proof) {
|
|
181
237
|
const payload = {
|
|
182
238
|
nulth: "zk-v1",
|
|
183
239
|
proof
|
|
184
240
|
};
|
|
185
|
-
return
|
|
241
|
+
return bytesToBase64(utf8ToBytes(JSON.stringify(payload)));
|
|
186
242
|
}
|
|
187
243
|
function decodeAuthSignature(encoded) {
|
|
188
|
-
const raw = JSON.parse(
|
|
244
|
+
const raw = JSON.parse(bytesToUtf8(base64ToBytes(encoded)));
|
|
189
245
|
if (typeof raw !== "object" || raw === null || raw.nulth !== "zk-v1") {
|
|
190
246
|
throw new Error("Invalid Nulth ZK auth signature");
|
|
191
247
|
}
|
|
@@ -199,16 +255,21 @@ var NulthClient = class {
|
|
|
199
255
|
this.policy = { ...config.policy };
|
|
200
256
|
this.prover = config.prover ?? DEFAULT_PROVER;
|
|
201
257
|
if (this.prover !== "mock") {
|
|
202
|
-
throw new
|
|
258
|
+
throw new NulthConfigError(
|
|
259
|
+
"unsupported_prover",
|
|
203
260
|
`NulthClient: unknown prover backend ${String(config.prover)}; only 'mock' is implemented`
|
|
204
261
|
);
|
|
205
262
|
}
|
|
206
263
|
if (config.network !== "testnet") {
|
|
207
|
-
throw new
|
|
264
|
+
throw new NulthConfigError(
|
|
265
|
+
"unsupported_network",
|
|
208
266
|
`NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
|
|
209
267
|
);
|
|
210
268
|
}
|
|
211
|
-
|
|
269
|
+
config.logger?.(
|
|
270
|
+
"warn",
|
|
271
|
+
"NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
|
|
272
|
+
);
|
|
212
273
|
}
|
|
213
274
|
config;
|
|
214
275
|
policy;
|
|
@@ -297,12 +358,144 @@ var NulthClient = class {
|
|
|
297
358
|
}
|
|
298
359
|
};
|
|
299
360
|
|
|
361
|
+
// src/scheme.ts
|
|
362
|
+
var import_stellar_sdk2 = require("@stellar/stellar-sdk");
|
|
363
|
+
var NULTH_SIGNATURE_VERSION = "zk-v1";
|
|
364
|
+
var NULTH_SIGNER_MARKER = /* @__PURE__ */ Symbol.for("routedock.nulth.signer");
|
|
365
|
+
function isNulthStellarSigner(signer) {
|
|
366
|
+
return signer[NULTH_SIGNER_MARKER] === NULTH_SIGNATURE_VERSION;
|
|
367
|
+
}
|
|
368
|
+
var SCVAL_TYPES = {
|
|
369
|
+
nulth: ["symbol", "string"],
|
|
370
|
+
version: ["symbol", "u32"],
|
|
371
|
+
proof: ["symbol", "bytes"],
|
|
372
|
+
auth_digest: ["symbol", "string"],
|
|
373
|
+
allowlist_commitment: ["symbol", "string"],
|
|
374
|
+
cap_commitment: ["symbol", "string"],
|
|
375
|
+
payee_hash: ["symbol", "string"],
|
|
376
|
+
amount_stroops: ["symbol", "i128"],
|
|
377
|
+
day_bucket: ["symbol", "u64"]
|
|
378
|
+
};
|
|
379
|
+
function nulthProofScVal(signature) {
|
|
380
|
+
if (signature.nulth !== NULTH_SIGNATURE_VERSION) {
|
|
381
|
+
throw new RangeError(`unsupported Nulth signature format ${String(signature.nulth)}`);
|
|
382
|
+
}
|
|
383
|
+
const { proof } = signature;
|
|
384
|
+
const inputs = proof.publicInputs;
|
|
385
|
+
return import_stellar_sdk2.xdr.ScVal.scvVec([
|
|
386
|
+
(0, import_stellar_sdk2.nativeToScVal)(
|
|
387
|
+
{
|
|
388
|
+
nulth: signature.nulth,
|
|
389
|
+
version: proof.version,
|
|
390
|
+
proof: base64ToBytes(proof.proof),
|
|
391
|
+
auth_digest: inputs.authDigest,
|
|
392
|
+
allowlist_commitment: inputs.allowlistCommitment,
|
|
393
|
+
cap_commitment: inputs.capCommitment,
|
|
394
|
+
payee_hash: inputs.payeeHash,
|
|
395
|
+
amount_stroops: BigInt(inputs.amountStroops),
|
|
396
|
+
day_bucket: BigInt(inputs.dayBucket)
|
|
397
|
+
},
|
|
398
|
+
{ type: SCVAL_TYPES }
|
|
399
|
+
)
|
|
400
|
+
]);
|
|
401
|
+
}
|
|
402
|
+
function requiredString(value, field) {
|
|
403
|
+
if (typeof value !== "string") {
|
|
404
|
+
throw new TypeError(`Nulth signature field ${field} is not a string`);
|
|
405
|
+
}
|
|
406
|
+
return value;
|
|
407
|
+
}
|
|
408
|
+
function requiredBytes(value, field) {
|
|
409
|
+
if (!(value instanceof Uint8Array)) {
|
|
410
|
+
throw new TypeError(`Nulth signature field ${field} is not bytes`);
|
|
411
|
+
}
|
|
412
|
+
return bytesToBase64(value);
|
|
413
|
+
}
|
|
414
|
+
function requiredNumber(value, field) {
|
|
415
|
+
const num = Number(value);
|
|
416
|
+
if (!Number.isInteger(num)) {
|
|
417
|
+
throw new TypeError(`Nulth signature field ${field} is not an integer`);
|
|
418
|
+
}
|
|
419
|
+
return num;
|
|
420
|
+
}
|
|
421
|
+
function requiredDecimal(value, field) {
|
|
422
|
+
if (typeof value === "string") return value;
|
|
423
|
+
if (typeof value === "bigint" || typeof value === "number") return value.toString();
|
|
424
|
+
throw new TypeError(`Nulth signature field ${field} is not a decimal value`);
|
|
425
|
+
}
|
|
426
|
+
function decodeNulthProofScVal(signature) {
|
|
427
|
+
if (signature.switch() !== import_stellar_sdk2.xdr.ScValType.scvVec()) {
|
|
428
|
+
throw new TypeError("Nulth signature is not a vec");
|
|
429
|
+
}
|
|
430
|
+
const entries = signature.vec();
|
|
431
|
+
if (!entries || entries.length !== 1) {
|
|
432
|
+
throw new TypeError(`Nulth signature vec must hold exactly one map, got ${entries?.length ?? 0}`);
|
|
433
|
+
}
|
|
434
|
+
const fields = (0, import_stellar_sdk2.scValToNative)(entries[0]);
|
|
435
|
+
const nulth = requiredString(fields.nulth, "nulth");
|
|
436
|
+
if (nulth !== NULTH_SIGNATURE_VERSION) {
|
|
437
|
+
throw new TypeError(`unsupported Nulth signature format ${nulth}`);
|
|
438
|
+
}
|
|
439
|
+
const proof = {
|
|
440
|
+
version: requiredNumber(fields.version, "version"),
|
|
441
|
+
proof: requiredBytes(fields.proof, "proof"),
|
|
442
|
+
publicInputs: {
|
|
443
|
+
authDigest: requiredString(fields.auth_digest, "auth_digest"),
|
|
444
|
+
allowlistCommitment: requiredString(fields.allowlist_commitment, "allowlist_commitment"),
|
|
445
|
+
capCommitment: requiredString(fields.cap_commitment, "cap_commitment"),
|
|
446
|
+
payeeHash: requiredString(fields.payee_hash, "payee_hash"),
|
|
447
|
+
amountStroops: requiredDecimal(fields.amount_stroops, "amount_stroops"),
|
|
448
|
+
dayBucket: requiredDecimal(fields.day_bucket, "day_bucket")
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
return { nulth: NULTH_SIGNATURE_VERSION, proof };
|
|
452
|
+
}
|
|
453
|
+
async function nulthAuthorizeEntry(entry, signer, validUntilLedgerSeq, networkPassphrase) {
|
|
454
|
+
if (entry.credentials().switch() !== import_stellar_sdk2.xdr.SorobanCredentialsType.sorobanCredentialsAddress()) {
|
|
455
|
+
return entry;
|
|
456
|
+
}
|
|
457
|
+
if (typeof signer !== "function") {
|
|
458
|
+
throw new TypeError(
|
|
459
|
+
"nulthAuthorizeEntry needs a callback that returns the zk-v1 proof envelope; a Keypair can only produce an ed25519 signature"
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
if (!networkPassphrase) {
|
|
463
|
+
throw new TypeError("nulthAuthorizeEntry needs the network passphrase to build the preimage");
|
|
464
|
+
}
|
|
465
|
+
const clone = import_stellar_sdk2.xdr.SorobanAuthorizationEntry.fromXDR(entry.toXDR());
|
|
466
|
+
const credentials = clone.credentials().address();
|
|
467
|
+
const address = import_stellar_sdk2.Address.fromScAddress(credentials.address()).toString();
|
|
468
|
+
if (!address.startsWith("C")) {
|
|
469
|
+
throw new TypeError(
|
|
470
|
+
`nulthAuthorizeEntry signs Nulth contract accounts (C...) only, got ${address}`
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
credentials.signatureExpirationLedger(validUntilLedgerSeq);
|
|
474
|
+
const preimage = import_stellar_sdk2.xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
|
|
475
|
+
new import_stellar_sdk2.xdr.HashIdPreimageSorobanAuthorization({
|
|
476
|
+
networkId: (0, import_stellar_sdk2.hash)(utf8ToBytes(networkPassphrase)),
|
|
477
|
+
nonce: credentials.nonce(),
|
|
478
|
+
invocation: clone.rootInvocation(),
|
|
479
|
+
signatureExpirationLedger: credentials.signatureExpirationLedger()
|
|
480
|
+
})
|
|
481
|
+
);
|
|
482
|
+
const signed = await signer(preimage);
|
|
483
|
+
credentials.signature(nulthProofScVal(decodeAuthSignature(envelopeBase64(signed))));
|
|
484
|
+
return clone;
|
|
485
|
+
}
|
|
486
|
+
function envelopeBase64(signed) {
|
|
487
|
+
if (signed instanceof Uint8Array) return bytesToBase64(signed);
|
|
488
|
+
if (signed instanceof ArrayBuffer) return bytesToBase64(new Uint8Array(signed));
|
|
489
|
+
throw new TypeError("Nulth signing callback must return the zk-v1 envelope as bytes");
|
|
490
|
+
}
|
|
491
|
+
|
|
300
492
|
// src/signer.ts
|
|
301
493
|
function createNulthSigner(config) {
|
|
302
494
|
const client = new NulthClient(config);
|
|
303
495
|
const nulthAccount = config.nulthAccount;
|
|
304
496
|
return {
|
|
305
497
|
address: nulthAccount,
|
|
498
|
+
[NULTH_SIGNER_MARKER]: NULTH_SIGNATURE_VERSION,
|
|
306
499
|
signAuthEntry: async (authEntry) => {
|
|
307
500
|
const ctx = config.paymentContext;
|
|
308
501
|
if (!ctx) {
|
|
@@ -353,7 +546,10 @@ function createPolicyState(input) {
|
|
|
353
546
|
// Annotate the CommonJS export names for ESM import in node:
|
|
354
547
|
0 && (module.exports = {
|
|
355
548
|
DEFAULT_PROVER,
|
|
549
|
+
NULTH_SIGNATURE_VERSION,
|
|
550
|
+
NULTH_SIGNER_MARKER,
|
|
356
551
|
NulthClient,
|
|
552
|
+
NulthConfigError,
|
|
357
553
|
NulthPolicyError,
|
|
358
554
|
USDC_DECIMALS,
|
|
359
555
|
assertAuthEntryMatchesContext,
|
|
@@ -364,9 +560,13 @@ function createPolicyState(input) {
|
|
|
364
560
|
createPolicyState,
|
|
365
561
|
dayBucket,
|
|
366
562
|
decodeAuthSignature,
|
|
563
|
+
decodeNulthProofScVal,
|
|
367
564
|
encodeAuthSignature,
|
|
368
565
|
hashPayee,
|
|
369
566
|
insecureMockProof,
|
|
567
|
+
isNulthStellarSigner,
|
|
568
|
+
nulthAuthorizeEntry,
|
|
569
|
+
nulthProofScVal,
|
|
370
570
|
paymentContextFromManifest,
|
|
371
571
|
setNulthPaymentContext,
|
|
372
572
|
sha256Hex,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SignAuthEntry } from '@stellar/stellar-sdk/contract';
|
|
2
|
+
import { xdr, SigningCallback, Keypair } from '@stellar/stellar-sdk';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Off-chain policy state for a Nulth ZK account.
|
|
@@ -53,6 +54,8 @@ interface NulthAuthSignature {
|
|
|
53
54
|
nulth: 'zk-v1';
|
|
54
55
|
proof: NulthProof;
|
|
55
56
|
}
|
|
57
|
+
/** Consumer-supplied diagnostic sink, compatible with RouteDockLogger. */
|
|
58
|
+
type NulthLogger = (level: 'debug' | 'info' | 'warn' | 'error', message: string, fields?: Record<string, unknown>) => void;
|
|
56
59
|
interface NulthClientConfig {
|
|
57
60
|
/** Nulth smart account contract address (C...) — the payer */
|
|
58
61
|
nulthAccount: string;
|
|
@@ -66,6 +69,13 @@ interface NulthClientConfig {
|
|
|
66
69
|
policy: NulthPolicyState;
|
|
67
70
|
/** Optional verifier contract deployed alongside the Nulth account */
|
|
68
71
|
verifierContract?: string;
|
|
72
|
+
/** Log sink for the mock-prover warning. Defaults to no-op (silent). */
|
|
73
|
+
logger?: NulthLogger;
|
|
74
|
+
}
|
|
75
|
+
/** Invalid network or prover configuration, distinct from policy failures. */
|
|
76
|
+
declare class NulthConfigError extends Error {
|
|
77
|
+
readonly code: 'unsupported_prover' | 'unsupported_network';
|
|
78
|
+
constructor(code: NulthConfigError['code'], message: string);
|
|
69
79
|
}
|
|
70
80
|
declare class NulthPolicyError extends Error {
|
|
71
81
|
readonly code: 'payee_not_allowed' | 'daily_cap_exceeded' | 'session_expired' | 'auth_entry_mismatch';
|
|
@@ -106,7 +116,7 @@ declare function usdcToStroops(amount: string): bigint;
|
|
|
106
116
|
declare function stroopsToUsdc(units: bigint): string;
|
|
107
117
|
|
|
108
118
|
/** SHA-256 hex digest */
|
|
109
|
-
declare function sha256Hex(data:
|
|
119
|
+
declare function sha256Hex(data: Uint8Array | string): string;
|
|
110
120
|
/** Commitment to the daily cap — cap value never appears on-chain */
|
|
111
121
|
declare function commitDailyCap(dailyCapStroops: bigint, witnessSecret: string): string;
|
|
112
122
|
/** Commitment to sorted allowlist — individual payees never published */
|
|
@@ -165,10 +175,60 @@ declare class NulthClient {
|
|
|
165
175
|
private enforcePolicy;
|
|
166
176
|
}
|
|
167
177
|
|
|
178
|
+
/**
|
|
179
|
+
* A Nulth proof travels in the Soroban auth entry's signature field instead of
|
|
180
|
+
* an ed25519 signature, so stellar-sdk's default `authorizeEntry` (which decodes
|
|
181
|
+
* the credential address as an ed25519 key and therefore throws
|
|
182
|
+
* `invalid version byte. expected 48, got 16` for a `C...` payer) cannot be used.
|
|
183
|
+
*/
|
|
184
|
+
/** Tag identifying the `zk-v1` signature layout, matching {@link NulthAuthSignature.nulth}. */
|
|
185
|
+
declare const NULTH_SIGNATURE_VERSION = "zk-v1";
|
|
186
|
+
/**
|
|
187
|
+
* Global-registry symbol set on signers produced by {@link createNulthSigner}, so
|
|
188
|
+
* an x402 client can pick the Nulth scheme without importing the signer factory.
|
|
189
|
+
* Registered globally because `@routedock/routedock` ships its own inlined copy of
|
|
190
|
+
* the signer (see #575); two copies of the same symbol must still compare equal.
|
|
191
|
+
*/
|
|
192
|
+
declare const NULTH_SIGNER_MARKER: unique symbol;
|
|
193
|
+
/** Anything carrying the marker is payer-side Nulth: contract account + proof signature. */
|
|
194
|
+
interface NulthSignerMarker {
|
|
195
|
+
readonly [NULTH_SIGNER_MARKER]: typeof NULTH_SIGNATURE_VERSION;
|
|
196
|
+
}
|
|
197
|
+
declare function isNulthStellarSigner(signer: object): boolean;
|
|
198
|
+
/**
|
|
199
|
+
* Encode a Nulth proof as the `SorobanAddressCredentials.signature` a Nulth
|
|
200
|
+
* account contract's `__check_auth` reads: `vec[map]`, the same shape Stellar uses
|
|
201
|
+
* for account signatures (`vec[{public_key, signature}]`), with the proof fields
|
|
202
|
+
* flattened into the map so the contract can map each one to a public input
|
|
203
|
+
* without parsing JSON.
|
|
204
|
+
*/
|
|
205
|
+
declare function nulthProofScVal(signature: NulthAuthSignature): xdr.ScVal;
|
|
206
|
+
/**
|
|
207
|
+
* Decode a signature ScVal written by {@link nulthProofScVal}. A Nulth account
|
|
208
|
+
* contract reads the same map; this is the client/provider-side mirror of that
|
|
209
|
+
* read, used to prove a signed entry round-trips.
|
|
210
|
+
*/
|
|
211
|
+
declare function decodeNulthProofScVal(signature: xdr.ScVal): NulthAuthSignature;
|
|
212
|
+
/**
|
|
213
|
+
* Build the signature the Nulth account authorises: sign the authorization
|
|
214
|
+
* preimage through the Nulth signer, which runs the off-chain policy checks and
|
|
215
|
+
* returns the `zk-v1` envelope, then write the proof into the credentials.
|
|
216
|
+
*
|
|
217
|
+
* Mirrors stellar-base's `authorizeEntry` in the parts that matter — the preimage
|
|
218
|
+
* is built identically, so the digest the proof binds is the one the network
|
|
219
|
+
* recomputes — and replaces the ed25519 verification and `{public_key, signature}`
|
|
220
|
+
* vec with {@link nulthProofScVal}. The parameter list matches
|
|
221
|
+
* `typeof authorizeEntry` deliberately, so it can be handed to
|
|
222
|
+
* `AssembledTransaction.signAuthEntries` as its `authorizeEntry` override.
|
|
223
|
+
*/
|
|
224
|
+
declare function nulthAuthorizeEntry(entry: xdr.SorobanAuthorizationEntry, signer: SigningCallback | Keypair, validUntilLedgerSeq: number, networkPassphrase?: string): Promise<xdr.SorobanAuthorizationEntry>;
|
|
225
|
+
|
|
168
226
|
/** Compatible with @x402/stellar ClientStellarSigner */
|
|
169
227
|
interface NulthStellarSigner {
|
|
170
228
|
address: string;
|
|
171
229
|
signAuthEntry: SignAuthEntry;
|
|
230
|
+
/** Marks this signer as Nulth so the x402 client selects the proof scheme. */
|
|
231
|
+
readonly [NULTH_SIGNER_MARKER]: typeof NULTH_SIGNATURE_VERSION;
|
|
172
232
|
}
|
|
173
233
|
interface NulthSignerConfig extends NulthClientConfig {
|
|
174
234
|
/** Pending payment context set before x402 signs auth entries */
|
|
@@ -205,4 +265,4 @@ declare function createPolicyState(input: {
|
|
|
205
265
|
ledgerSequence?: number;
|
|
206
266
|
}): NulthPolicyState;
|
|
207
267
|
|
|
208
|
-
export { DEFAULT_PROVER, type NulthAuthSignature, NulthClient, type NulthClientConfig, NulthPolicyError, type NulthPolicyState, type NulthProof, type NulthSignerConfig, type NulthStellarSigner, type PaymentAuthContext, type ProverBackend, USDC_DECIMALS, assertAuthEntryMatchesContext, authDigestFromEntry, commitAllowlist, commitDailyCap, createNulthSigner, createPolicyState, dayBucket, decodeAuthSignature, encodeAuthSignature, hashPayee, insecureMockProof, paymentContextFromManifest, setNulthPaymentContext, sha256Hex, stroopsToUsdc, usdcToStroops };
|
|
268
|
+
export { DEFAULT_PROVER, NULTH_SIGNATURE_VERSION, NULTH_SIGNER_MARKER, type NulthAuthSignature, NulthClient, type NulthClientConfig, NulthConfigError, type NulthLogger, NulthPolicyError, type NulthPolicyState, type NulthProof, type NulthSignerConfig, type NulthSignerMarker, type NulthStellarSigner, type PaymentAuthContext, type ProverBackend, USDC_DECIMALS, assertAuthEntryMatchesContext, authDigestFromEntry, commitAllowlist, commitDailyCap, createNulthSigner, createPolicyState, dayBucket, decodeAuthSignature, decodeNulthProofScVal, encodeAuthSignature, hashPayee, insecureMockProof, isNulthStellarSigner, nulthAuthorizeEntry, nulthProofScVal, paymentContextFromManifest, setNulthPaymentContext, sha256Hex, stroopsToUsdc, usdcToStroops };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SignAuthEntry } from '@stellar/stellar-sdk/contract';
|
|
2
|
+
import { xdr, SigningCallback, Keypair } from '@stellar/stellar-sdk';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Off-chain policy state for a Nulth ZK account.
|
|
@@ -53,6 +54,8 @@ interface NulthAuthSignature {
|
|
|
53
54
|
nulth: 'zk-v1';
|
|
54
55
|
proof: NulthProof;
|
|
55
56
|
}
|
|
57
|
+
/** Consumer-supplied diagnostic sink, compatible with RouteDockLogger. */
|
|
58
|
+
type NulthLogger = (level: 'debug' | 'info' | 'warn' | 'error', message: string, fields?: Record<string, unknown>) => void;
|
|
56
59
|
interface NulthClientConfig {
|
|
57
60
|
/** Nulth smart account contract address (C...) — the payer */
|
|
58
61
|
nulthAccount: string;
|
|
@@ -66,6 +69,13 @@ interface NulthClientConfig {
|
|
|
66
69
|
policy: NulthPolicyState;
|
|
67
70
|
/** Optional verifier contract deployed alongside the Nulth account */
|
|
68
71
|
verifierContract?: string;
|
|
72
|
+
/** Log sink for the mock-prover warning. Defaults to no-op (silent). */
|
|
73
|
+
logger?: NulthLogger;
|
|
74
|
+
}
|
|
75
|
+
/** Invalid network or prover configuration, distinct from policy failures. */
|
|
76
|
+
declare class NulthConfigError extends Error {
|
|
77
|
+
readonly code: 'unsupported_prover' | 'unsupported_network';
|
|
78
|
+
constructor(code: NulthConfigError['code'], message: string);
|
|
69
79
|
}
|
|
70
80
|
declare class NulthPolicyError extends Error {
|
|
71
81
|
readonly code: 'payee_not_allowed' | 'daily_cap_exceeded' | 'session_expired' | 'auth_entry_mismatch';
|
|
@@ -106,7 +116,7 @@ declare function usdcToStroops(amount: string): bigint;
|
|
|
106
116
|
declare function stroopsToUsdc(units: bigint): string;
|
|
107
117
|
|
|
108
118
|
/** SHA-256 hex digest */
|
|
109
|
-
declare function sha256Hex(data:
|
|
119
|
+
declare function sha256Hex(data: Uint8Array | string): string;
|
|
110
120
|
/** Commitment to the daily cap — cap value never appears on-chain */
|
|
111
121
|
declare function commitDailyCap(dailyCapStroops: bigint, witnessSecret: string): string;
|
|
112
122
|
/** Commitment to sorted allowlist — individual payees never published */
|
|
@@ -165,10 +175,60 @@ declare class NulthClient {
|
|
|
165
175
|
private enforcePolicy;
|
|
166
176
|
}
|
|
167
177
|
|
|
178
|
+
/**
|
|
179
|
+
* A Nulth proof travels in the Soroban auth entry's signature field instead of
|
|
180
|
+
* an ed25519 signature, so stellar-sdk's default `authorizeEntry` (which decodes
|
|
181
|
+
* the credential address as an ed25519 key and therefore throws
|
|
182
|
+
* `invalid version byte. expected 48, got 16` for a `C...` payer) cannot be used.
|
|
183
|
+
*/
|
|
184
|
+
/** Tag identifying the `zk-v1` signature layout, matching {@link NulthAuthSignature.nulth}. */
|
|
185
|
+
declare const NULTH_SIGNATURE_VERSION = "zk-v1";
|
|
186
|
+
/**
|
|
187
|
+
* Global-registry symbol set on signers produced by {@link createNulthSigner}, so
|
|
188
|
+
* an x402 client can pick the Nulth scheme without importing the signer factory.
|
|
189
|
+
* Registered globally because `@routedock/routedock` ships its own inlined copy of
|
|
190
|
+
* the signer (see #575); two copies of the same symbol must still compare equal.
|
|
191
|
+
*/
|
|
192
|
+
declare const NULTH_SIGNER_MARKER: unique symbol;
|
|
193
|
+
/** Anything carrying the marker is payer-side Nulth: contract account + proof signature. */
|
|
194
|
+
interface NulthSignerMarker {
|
|
195
|
+
readonly [NULTH_SIGNER_MARKER]: typeof NULTH_SIGNATURE_VERSION;
|
|
196
|
+
}
|
|
197
|
+
declare function isNulthStellarSigner(signer: object): boolean;
|
|
198
|
+
/**
|
|
199
|
+
* Encode a Nulth proof as the `SorobanAddressCredentials.signature` a Nulth
|
|
200
|
+
* account contract's `__check_auth` reads: `vec[map]`, the same shape Stellar uses
|
|
201
|
+
* for account signatures (`vec[{public_key, signature}]`), with the proof fields
|
|
202
|
+
* flattened into the map so the contract can map each one to a public input
|
|
203
|
+
* without parsing JSON.
|
|
204
|
+
*/
|
|
205
|
+
declare function nulthProofScVal(signature: NulthAuthSignature): xdr.ScVal;
|
|
206
|
+
/**
|
|
207
|
+
* Decode a signature ScVal written by {@link nulthProofScVal}. A Nulth account
|
|
208
|
+
* contract reads the same map; this is the client/provider-side mirror of that
|
|
209
|
+
* read, used to prove a signed entry round-trips.
|
|
210
|
+
*/
|
|
211
|
+
declare function decodeNulthProofScVal(signature: xdr.ScVal): NulthAuthSignature;
|
|
212
|
+
/**
|
|
213
|
+
* Build the signature the Nulth account authorises: sign the authorization
|
|
214
|
+
* preimage through the Nulth signer, which runs the off-chain policy checks and
|
|
215
|
+
* returns the `zk-v1` envelope, then write the proof into the credentials.
|
|
216
|
+
*
|
|
217
|
+
* Mirrors stellar-base's `authorizeEntry` in the parts that matter — the preimage
|
|
218
|
+
* is built identically, so the digest the proof binds is the one the network
|
|
219
|
+
* recomputes — and replaces the ed25519 verification and `{public_key, signature}`
|
|
220
|
+
* vec with {@link nulthProofScVal}. The parameter list matches
|
|
221
|
+
* `typeof authorizeEntry` deliberately, so it can be handed to
|
|
222
|
+
* `AssembledTransaction.signAuthEntries` as its `authorizeEntry` override.
|
|
223
|
+
*/
|
|
224
|
+
declare function nulthAuthorizeEntry(entry: xdr.SorobanAuthorizationEntry, signer: SigningCallback | Keypair, validUntilLedgerSeq: number, networkPassphrase?: string): Promise<xdr.SorobanAuthorizationEntry>;
|
|
225
|
+
|
|
168
226
|
/** Compatible with @x402/stellar ClientStellarSigner */
|
|
169
227
|
interface NulthStellarSigner {
|
|
170
228
|
address: string;
|
|
171
229
|
signAuthEntry: SignAuthEntry;
|
|
230
|
+
/** Marks this signer as Nulth so the x402 client selects the proof scheme. */
|
|
231
|
+
readonly [NULTH_SIGNER_MARKER]: typeof NULTH_SIGNATURE_VERSION;
|
|
172
232
|
}
|
|
173
233
|
interface NulthSignerConfig extends NulthClientConfig {
|
|
174
234
|
/** Pending payment context set before x402 signs auth entries */
|
|
@@ -205,4 +265,4 @@ declare function createPolicyState(input: {
|
|
|
205
265
|
ledgerSequence?: number;
|
|
206
266
|
}): NulthPolicyState;
|
|
207
267
|
|
|
208
|
-
export { DEFAULT_PROVER, type NulthAuthSignature, NulthClient, type NulthClientConfig, NulthPolicyError, type NulthPolicyState, type NulthProof, type NulthSignerConfig, type NulthStellarSigner, type PaymentAuthContext, type ProverBackend, USDC_DECIMALS, assertAuthEntryMatchesContext, authDigestFromEntry, commitAllowlist, commitDailyCap, createNulthSigner, createPolicyState, dayBucket, decodeAuthSignature, encodeAuthSignature, hashPayee, insecureMockProof, paymentContextFromManifest, setNulthPaymentContext, sha256Hex, stroopsToUsdc, usdcToStroops };
|
|
268
|
+
export { DEFAULT_PROVER, NULTH_SIGNATURE_VERSION, NULTH_SIGNER_MARKER, type NulthAuthSignature, NulthClient, type NulthClientConfig, NulthConfigError, type NulthLogger, NulthPolicyError, type NulthPolicyState, type NulthProof, type NulthSignerConfig, type NulthSignerMarker, type NulthStellarSigner, type PaymentAuthContext, type ProverBackend, USDC_DECIMALS, assertAuthEntryMatchesContext, authDigestFromEntry, commitAllowlist, commitDailyCap, createNulthSigner, createPolicyState, dayBucket, decodeAuthSignature, decodeNulthProofScVal, encodeAuthSignature, hashPayee, insecureMockProof, isNulthStellarSigner, nulthAuthorizeEntry, nulthProofScVal, paymentContextFromManifest, setNulthPaymentContext, sha256Hex, stroopsToUsdc, usdcToStroops };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
// src/types.ts
|
|
2
|
+
var NulthConfigError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "NulthConfigError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
2
10
|
var NulthPolicyError = class extends Error {
|
|
3
11
|
code;
|
|
4
12
|
constructor(code, message) {
|
|
@@ -9,9 +17,51 @@ var NulthPolicyError = class extends Error {
|
|
|
9
17
|
};
|
|
10
18
|
|
|
11
19
|
// src/proof.ts
|
|
12
|
-
import { createHash } from "crypto";
|
|
13
20
|
import { Address, hash, scValToNative, xdr } from "@stellar/stellar-sdk";
|
|
14
21
|
|
|
22
|
+
// src/encoding.ts
|
|
23
|
+
import { sha256 as nobleSha256, sha512 as nobleSha512 } from "@noble/hashes/sha2.js";
|
|
24
|
+
var encoder = new TextEncoder();
|
|
25
|
+
var decoder = new TextDecoder();
|
|
26
|
+
function utf8ToBytes(text) {
|
|
27
|
+
return encoder.encode(text);
|
|
28
|
+
}
|
|
29
|
+
function bytesToUtf8(bytes) {
|
|
30
|
+
return decoder.decode(bytes);
|
|
31
|
+
}
|
|
32
|
+
function bytesToBase64(bytes) {
|
|
33
|
+
const chunk = 32768;
|
|
34
|
+
let binary = "";
|
|
35
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
36
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
37
|
+
}
|
|
38
|
+
return btoa(binary);
|
|
39
|
+
}
|
|
40
|
+
function base64ToBytes(b64) {
|
|
41
|
+
const end = b64.indexOf("=");
|
|
42
|
+
let clean = (end === -1 ? b64 : b64.slice(0, end)).replace(/-/g, "+").replace(/_/g, "/").replace(/[^A-Za-z0-9+/]/g, "");
|
|
43
|
+
if (clean.length % 4 === 1) clean = clean.slice(0, -1);
|
|
44
|
+
const binary = atob(clean);
|
|
45
|
+
const bytes = new Uint8Array(binary.length);
|
|
46
|
+
for (let i = 0; i < binary.length; i++) {
|
|
47
|
+
bytes[i] = binary.charCodeAt(i);
|
|
48
|
+
}
|
|
49
|
+
return bytes;
|
|
50
|
+
}
|
|
51
|
+
function bytesToHex(bytes) {
|
|
52
|
+
let out = "";
|
|
53
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
54
|
+
out += bytes[i].toString(16).padStart(2, "0");
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
function sha256Bytes(data) {
|
|
59
|
+
return nobleSha256(typeof data === "string" ? utf8ToBytes(data) : data);
|
|
60
|
+
}
|
|
61
|
+
function sha512Bytes(data) {
|
|
62
|
+
return nobleSha512(typeof data === "string" ? utf8ToBytes(data) : data);
|
|
63
|
+
}
|
|
64
|
+
|
|
15
65
|
// src/usdc.ts
|
|
16
66
|
var USDC_DECIMALS = 7;
|
|
17
67
|
var USDC_SCALE = 10n ** BigInt(USDC_DECIMALS);
|
|
@@ -47,8 +97,7 @@ function stroopsToUsdc(units) {
|
|
|
47
97
|
|
|
48
98
|
// src/proof.ts
|
|
49
99
|
function sha256Hex(data) {
|
|
50
|
-
|
|
51
|
-
return createHash("sha256").update(buf).digest("hex");
|
|
100
|
+
return bytesToHex(sha256Bytes(data));
|
|
52
101
|
}
|
|
53
102
|
function commitDailyCap(dailyCapStroops, witnessSecret) {
|
|
54
103
|
return sha256Hex(`${witnessSecret}:cap:${dailyCapStroops.toString()}`);
|
|
@@ -64,7 +113,7 @@ function dayBucket(ledgerSequence) {
|
|
|
64
113
|
return Math.floor(ledgerSequence / 17280);
|
|
65
114
|
}
|
|
66
115
|
function authDigestFromEntry(authEntryBase64) {
|
|
67
|
-
const entryHash = hash(
|
|
116
|
+
const entryHash = hash(base64ToBytes(authEntryBase64));
|
|
68
117
|
return sha256Hex(entryHash);
|
|
69
118
|
}
|
|
70
119
|
function assertAuthEntryMatchesContext(authEntry, from, ctx) {
|
|
@@ -130,17 +179,17 @@ function assertAuthEntryMatchesContext(authEntry, from, ctx) {
|
|
|
130
179
|
}
|
|
131
180
|
var DEFAULT_PROVER = "mock";
|
|
132
181
|
function insecureMockProof(preimage) {
|
|
133
|
-
return
|
|
182
|
+
return bytesToBase64(sha512Bytes(`nulth:${preimage}`));
|
|
134
183
|
}
|
|
135
184
|
function encodeAuthSignature(proof) {
|
|
136
185
|
const payload = {
|
|
137
186
|
nulth: "zk-v1",
|
|
138
187
|
proof
|
|
139
188
|
};
|
|
140
|
-
return
|
|
189
|
+
return bytesToBase64(utf8ToBytes(JSON.stringify(payload)));
|
|
141
190
|
}
|
|
142
191
|
function decodeAuthSignature(encoded) {
|
|
143
|
-
const raw = JSON.parse(
|
|
192
|
+
const raw = JSON.parse(bytesToUtf8(base64ToBytes(encoded)));
|
|
144
193
|
if (typeof raw !== "object" || raw === null || raw.nulth !== "zk-v1") {
|
|
145
194
|
throw new Error("Invalid Nulth ZK auth signature");
|
|
146
195
|
}
|
|
@@ -154,16 +203,21 @@ var NulthClient = class {
|
|
|
154
203
|
this.policy = { ...config.policy };
|
|
155
204
|
this.prover = config.prover ?? DEFAULT_PROVER;
|
|
156
205
|
if (this.prover !== "mock") {
|
|
157
|
-
throw new
|
|
206
|
+
throw new NulthConfigError(
|
|
207
|
+
"unsupported_prover",
|
|
158
208
|
`NulthClient: unknown prover backend ${String(config.prover)}; only 'mock' is implemented`
|
|
159
209
|
);
|
|
160
210
|
}
|
|
161
211
|
if (config.network !== "testnet") {
|
|
162
|
-
throw new
|
|
212
|
+
throw new NulthConfigError(
|
|
213
|
+
"unsupported_network",
|
|
163
214
|
`NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
|
|
164
215
|
);
|
|
165
216
|
}
|
|
166
|
-
|
|
217
|
+
config.logger?.(
|
|
218
|
+
"warn",
|
|
219
|
+
"NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
|
|
220
|
+
);
|
|
167
221
|
}
|
|
168
222
|
config;
|
|
169
223
|
policy;
|
|
@@ -252,12 +306,144 @@ var NulthClient = class {
|
|
|
252
306
|
}
|
|
253
307
|
};
|
|
254
308
|
|
|
309
|
+
// src/scheme.ts
|
|
310
|
+
import { Address as Address2, hash as hash2, nativeToScVal, scValToNative as scValToNative2, xdr as xdr2 } from "@stellar/stellar-sdk";
|
|
311
|
+
var NULTH_SIGNATURE_VERSION = "zk-v1";
|
|
312
|
+
var NULTH_SIGNER_MARKER = /* @__PURE__ */ Symbol.for("routedock.nulth.signer");
|
|
313
|
+
function isNulthStellarSigner(signer) {
|
|
314
|
+
return signer[NULTH_SIGNER_MARKER] === NULTH_SIGNATURE_VERSION;
|
|
315
|
+
}
|
|
316
|
+
var SCVAL_TYPES = {
|
|
317
|
+
nulth: ["symbol", "string"],
|
|
318
|
+
version: ["symbol", "u32"],
|
|
319
|
+
proof: ["symbol", "bytes"],
|
|
320
|
+
auth_digest: ["symbol", "string"],
|
|
321
|
+
allowlist_commitment: ["symbol", "string"],
|
|
322
|
+
cap_commitment: ["symbol", "string"],
|
|
323
|
+
payee_hash: ["symbol", "string"],
|
|
324
|
+
amount_stroops: ["symbol", "i128"],
|
|
325
|
+
day_bucket: ["symbol", "u64"]
|
|
326
|
+
};
|
|
327
|
+
function nulthProofScVal(signature) {
|
|
328
|
+
if (signature.nulth !== NULTH_SIGNATURE_VERSION) {
|
|
329
|
+
throw new RangeError(`unsupported Nulth signature format ${String(signature.nulth)}`);
|
|
330
|
+
}
|
|
331
|
+
const { proof } = signature;
|
|
332
|
+
const inputs = proof.publicInputs;
|
|
333
|
+
return xdr2.ScVal.scvVec([
|
|
334
|
+
nativeToScVal(
|
|
335
|
+
{
|
|
336
|
+
nulth: signature.nulth,
|
|
337
|
+
version: proof.version,
|
|
338
|
+
proof: base64ToBytes(proof.proof),
|
|
339
|
+
auth_digest: inputs.authDigest,
|
|
340
|
+
allowlist_commitment: inputs.allowlistCommitment,
|
|
341
|
+
cap_commitment: inputs.capCommitment,
|
|
342
|
+
payee_hash: inputs.payeeHash,
|
|
343
|
+
amount_stroops: BigInt(inputs.amountStroops),
|
|
344
|
+
day_bucket: BigInt(inputs.dayBucket)
|
|
345
|
+
},
|
|
346
|
+
{ type: SCVAL_TYPES }
|
|
347
|
+
)
|
|
348
|
+
]);
|
|
349
|
+
}
|
|
350
|
+
function requiredString(value, field) {
|
|
351
|
+
if (typeof value !== "string") {
|
|
352
|
+
throw new TypeError(`Nulth signature field ${field} is not a string`);
|
|
353
|
+
}
|
|
354
|
+
return value;
|
|
355
|
+
}
|
|
356
|
+
function requiredBytes(value, field) {
|
|
357
|
+
if (!(value instanceof Uint8Array)) {
|
|
358
|
+
throw new TypeError(`Nulth signature field ${field} is not bytes`);
|
|
359
|
+
}
|
|
360
|
+
return bytesToBase64(value);
|
|
361
|
+
}
|
|
362
|
+
function requiredNumber(value, field) {
|
|
363
|
+
const num = Number(value);
|
|
364
|
+
if (!Number.isInteger(num)) {
|
|
365
|
+
throw new TypeError(`Nulth signature field ${field} is not an integer`);
|
|
366
|
+
}
|
|
367
|
+
return num;
|
|
368
|
+
}
|
|
369
|
+
function requiredDecimal(value, field) {
|
|
370
|
+
if (typeof value === "string") return value;
|
|
371
|
+
if (typeof value === "bigint" || typeof value === "number") return value.toString();
|
|
372
|
+
throw new TypeError(`Nulth signature field ${field} is not a decimal value`);
|
|
373
|
+
}
|
|
374
|
+
function decodeNulthProofScVal(signature) {
|
|
375
|
+
if (signature.switch() !== xdr2.ScValType.scvVec()) {
|
|
376
|
+
throw new TypeError("Nulth signature is not a vec");
|
|
377
|
+
}
|
|
378
|
+
const entries = signature.vec();
|
|
379
|
+
if (!entries || entries.length !== 1) {
|
|
380
|
+
throw new TypeError(`Nulth signature vec must hold exactly one map, got ${entries?.length ?? 0}`);
|
|
381
|
+
}
|
|
382
|
+
const fields = scValToNative2(entries[0]);
|
|
383
|
+
const nulth = requiredString(fields.nulth, "nulth");
|
|
384
|
+
if (nulth !== NULTH_SIGNATURE_VERSION) {
|
|
385
|
+
throw new TypeError(`unsupported Nulth signature format ${nulth}`);
|
|
386
|
+
}
|
|
387
|
+
const proof = {
|
|
388
|
+
version: requiredNumber(fields.version, "version"),
|
|
389
|
+
proof: requiredBytes(fields.proof, "proof"),
|
|
390
|
+
publicInputs: {
|
|
391
|
+
authDigest: requiredString(fields.auth_digest, "auth_digest"),
|
|
392
|
+
allowlistCommitment: requiredString(fields.allowlist_commitment, "allowlist_commitment"),
|
|
393
|
+
capCommitment: requiredString(fields.cap_commitment, "cap_commitment"),
|
|
394
|
+
payeeHash: requiredString(fields.payee_hash, "payee_hash"),
|
|
395
|
+
amountStroops: requiredDecimal(fields.amount_stroops, "amount_stroops"),
|
|
396
|
+
dayBucket: requiredDecimal(fields.day_bucket, "day_bucket")
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
return { nulth: NULTH_SIGNATURE_VERSION, proof };
|
|
400
|
+
}
|
|
401
|
+
async function nulthAuthorizeEntry(entry, signer, validUntilLedgerSeq, networkPassphrase) {
|
|
402
|
+
if (entry.credentials().switch() !== xdr2.SorobanCredentialsType.sorobanCredentialsAddress()) {
|
|
403
|
+
return entry;
|
|
404
|
+
}
|
|
405
|
+
if (typeof signer !== "function") {
|
|
406
|
+
throw new TypeError(
|
|
407
|
+
"nulthAuthorizeEntry needs a callback that returns the zk-v1 proof envelope; a Keypair can only produce an ed25519 signature"
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
if (!networkPassphrase) {
|
|
411
|
+
throw new TypeError("nulthAuthorizeEntry needs the network passphrase to build the preimage");
|
|
412
|
+
}
|
|
413
|
+
const clone = xdr2.SorobanAuthorizationEntry.fromXDR(entry.toXDR());
|
|
414
|
+
const credentials = clone.credentials().address();
|
|
415
|
+
const address = Address2.fromScAddress(credentials.address()).toString();
|
|
416
|
+
if (!address.startsWith("C")) {
|
|
417
|
+
throw new TypeError(
|
|
418
|
+
`nulthAuthorizeEntry signs Nulth contract accounts (C...) only, got ${address}`
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
credentials.signatureExpirationLedger(validUntilLedgerSeq);
|
|
422
|
+
const preimage = xdr2.HashIdPreimage.envelopeTypeSorobanAuthorization(
|
|
423
|
+
new xdr2.HashIdPreimageSorobanAuthorization({
|
|
424
|
+
networkId: hash2(utf8ToBytes(networkPassphrase)),
|
|
425
|
+
nonce: credentials.nonce(),
|
|
426
|
+
invocation: clone.rootInvocation(),
|
|
427
|
+
signatureExpirationLedger: credentials.signatureExpirationLedger()
|
|
428
|
+
})
|
|
429
|
+
);
|
|
430
|
+
const signed = await signer(preimage);
|
|
431
|
+
credentials.signature(nulthProofScVal(decodeAuthSignature(envelopeBase64(signed))));
|
|
432
|
+
return clone;
|
|
433
|
+
}
|
|
434
|
+
function envelopeBase64(signed) {
|
|
435
|
+
if (signed instanceof Uint8Array) return bytesToBase64(signed);
|
|
436
|
+
if (signed instanceof ArrayBuffer) return bytesToBase64(new Uint8Array(signed));
|
|
437
|
+
throw new TypeError("Nulth signing callback must return the zk-v1 envelope as bytes");
|
|
438
|
+
}
|
|
439
|
+
|
|
255
440
|
// src/signer.ts
|
|
256
441
|
function createNulthSigner(config) {
|
|
257
442
|
const client = new NulthClient(config);
|
|
258
443
|
const nulthAccount = config.nulthAccount;
|
|
259
444
|
return {
|
|
260
445
|
address: nulthAccount,
|
|
446
|
+
[NULTH_SIGNER_MARKER]: NULTH_SIGNATURE_VERSION,
|
|
261
447
|
signAuthEntry: async (authEntry) => {
|
|
262
448
|
const ctx = config.paymentContext;
|
|
263
449
|
if (!ctx) {
|
|
@@ -307,7 +493,10 @@ function createPolicyState(input) {
|
|
|
307
493
|
}
|
|
308
494
|
export {
|
|
309
495
|
DEFAULT_PROVER,
|
|
496
|
+
NULTH_SIGNATURE_VERSION,
|
|
497
|
+
NULTH_SIGNER_MARKER,
|
|
310
498
|
NulthClient,
|
|
499
|
+
NulthConfigError,
|
|
311
500
|
NulthPolicyError,
|
|
312
501
|
USDC_DECIMALS,
|
|
313
502
|
assertAuthEntryMatchesContext,
|
|
@@ -318,9 +507,13 @@ export {
|
|
|
318
507
|
createPolicyState,
|
|
319
508
|
dayBucket,
|
|
320
509
|
decodeAuthSignature,
|
|
510
|
+
decodeNulthProofScVal,
|
|
321
511
|
encodeAuthSignature,
|
|
322
512
|
hashPayee,
|
|
323
513
|
insecureMockProof,
|
|
514
|
+
isNulthStellarSigner,
|
|
515
|
+
nulthAuthorizeEntry,
|
|
516
|
+
nulthProofScVal,
|
|
324
517
|
paymentContextFromManifest,
|
|
325
518
|
setNulthPaymentContext,
|
|
326
519
|
sha256Hex,
|
package/package.json
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@routedock/nulth-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Off-chain ZK proof builder for Nulth smart accounts on Stellar",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/winsznx/routedock/tree/main/packages/nulth-sdk#readme",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/winsznx/routedock/issues"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"provenance": true
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/winsznx/routedock.git",
|
|
16
|
+
"directory": "packages/nulth-sdk"
|
|
17
|
+
},
|
|
6
18
|
"type": "module",
|
|
7
19
|
"main": "dist/index.cjs",
|
|
8
20
|
"module": "dist/index.js",
|
|
@@ -24,6 +36,7 @@
|
|
|
24
36
|
}
|
|
25
37
|
},
|
|
26
38
|
"dependencies": {
|
|
39
|
+
"@noble/hashes": "^1.8.0",
|
|
27
40
|
"@stellar/stellar-sdk": "^14.6.1"
|
|
28
41
|
},
|
|
29
42
|
"devDependencies": {
|
|
@@ -32,9 +45,23 @@
|
|
|
32
45
|
"tsx": "^4.22.3",
|
|
33
46
|
"typescript": "^5.4.5"
|
|
34
47
|
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=22"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"stellar",
|
|
53
|
+
"soroban",
|
|
54
|
+
"usdc",
|
|
55
|
+
"x402",
|
|
56
|
+
"mpp",
|
|
57
|
+
"payments",
|
|
58
|
+
"ai-agents",
|
|
59
|
+
"zk"
|
|
60
|
+
],
|
|
35
61
|
"scripts": {
|
|
36
62
|
"build": "tsup",
|
|
37
63
|
"typecheck": "tsc --noEmit",
|
|
38
|
-
"test": "tsx --test src/__tests__
|
|
64
|
+
"test": "tsx --test src/__tests__/*.test.ts",
|
|
65
|
+
"check:readme": "node ../../scripts/check-nulth-readme.mjs"
|
|
39
66
|
}
|
|
40
67
|
}
|