@chainpayhq/sdk 0.0.0-stage → 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 +21 -0
- package/README.md +42 -2
- package/dist/accounts.d.ts +23 -0
- package/dist/accounts.js +148 -0
- package/dist/cards/accounts.d.ts +156 -0
- package/dist/cards/accounts.js +346 -0
- package/dist/cards/api.d.ts +379 -0
- package/dist/cards/api.js +241 -0
- package/dist/cards/commitment.d.ts +86 -0
- package/dist/cards/commitment.js +288 -0
- package/dist/cards/constants.d.ts +160 -0
- package/dist/cards/constants.js +163 -0
- package/dist/cards/draft.d.ts +57 -0
- package/dist/cards/draft.js +131 -0
- package/dist/cards/evidence.d.ts +69 -0
- package/dist/cards/evidence.js +41 -0
- package/dist/cards/hash.d.ts +9 -0
- package/dist/cards/hash.js +47 -0
- package/dist/cards/index.d.ts +14 -0
- package/dist/cards/index.js +14 -0
- package/dist/cards/instructions.d.ts +302 -0
- package/dist/cards/instructions.js +474 -0
- package/dist/cards/layout.d.ts +33 -0
- package/dist/cards/layout.js +137 -0
- package/dist/cards/math.d.ts +67 -0
- package/dist/cards/math.js +138 -0
- package/dist/cards/merchants.d.ts +23 -0
- package/dist/cards/merchants.js +38 -0
- package/dist/cards/pda.d.ts +42 -0
- package/dist/cards/pda.js +93 -0
- package/dist/cards/private-repayment.d.ts +198 -0
- package/dist/cards/private-repayment.js +485 -0
- package/dist/cards/redact.d.ts +18 -0
- package/dist/cards/redact.js +103 -0
- package/dist/cards/tee.d.ts +253 -0
- package/dist/cards/tee.js +605 -0
- package/dist/cli.d.ts +31 -0
- package/dist/cli.js +351 -0
- package/dist/client.d.ts +80 -0
- package/dist/client.js +493 -0
- package/dist/constants.d.ts +44 -0
- package/dist/constants.js +43 -0
- package/dist/crossmint-adapt.d.ts +43 -0
- package/dist/crossmint-adapt.js +68 -0
- package/dist/crossmint-order.d.ts +220 -0
- package/dist/crossmint-order.js +638 -0
- package/dist/delivery.d.ts +66 -0
- package/dist/delivery.js +232 -0
- package/dist/encoding.d.ts +51 -0
- package/dist/encoding.js +128 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +26 -0
- package/dist/known-assets.d.ts +30 -0
- package/dist/known-assets.js +49 -0
- package/dist/mandate-request.d.ts +123 -0
- package/dist/mandate-request.js +401 -0
- package/dist/mandate.d.ts +44 -0
- package/dist/mandate.js +157 -0
- package/dist/ops-snapshot.d.ts +157 -0
- package/dist/ops-snapshot.js +356 -0
- package/dist/payment-request.d.ts +37 -0
- package/dist/payment-request.js +218 -0
- package/dist/payment.d.ts +40 -0
- package/dist/payment.js +214 -0
- package/dist/pda.d.ts +13 -0
- package/dist/pda.js +36 -0
- package/dist/receipt-export.d.ts +55 -0
- package/dist/receipt-export.js +149 -0
- package/dist/receipt.d.ts +108 -0
- package/dist/receipt.js +213 -0
- package/dist/solana.d.ts +5 -0
- package/dist/solana.js +20 -0
- package/dist/token-capabilities.d.ts +9 -0
- package/dist/token-capabilities.js +139 -0
- package/dist/token.d.ts +18 -0
- package/dist/token.js +55 -0
- package/dist/transaction-reader.d.ts +10 -0
- package/dist/transaction-reader.js +18 -0
- package/dist/transaction-v1.d.ts +18 -0
- package/dist/transaction-v1.js +73 -0
- package/dist/types.d.ts +267 -0
- package/dist/types.js +1 -0
- package/dist/x402-adapt.d.ts +20 -0
- package/dist/x402-adapt.js +62 -0
- package/dist/x402-challenge.d.ts +116 -0
- package/dist/x402-challenge.js +385 -0
- package/dist/x402.d.ts +18 -0
- package/dist/x402.js +35 -0
- package/package.json +54 -4
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Address } from "../types.js";
|
|
2
|
+
import type { CardCommitment, CardPeriod, CardPolicy } from "./accounts.js";
|
|
3
|
+
export declare const COMMITMENT_LEAF_COUNT = 16;
|
|
4
|
+
export type CommitmentLeafIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15;
|
|
5
|
+
export type DisclosedLeaf = {
|
|
6
|
+
i: number;
|
|
7
|
+
/** hex of the leaf's value bytes. */
|
|
8
|
+
value: string;
|
|
9
|
+
/** hex of sha256(master_salt || i). Reveals nothing about other leaves. */
|
|
10
|
+
leafSalt: string;
|
|
11
|
+
/** 4 sibling hashes (hex), bottom-up. */
|
|
12
|
+
proof: string[];
|
|
13
|
+
};
|
|
14
|
+
export type DisclosureBundle = {
|
|
15
|
+
v: 1;
|
|
16
|
+
/** CardBinding address (base58); the commitment PDA is derived from it. */
|
|
17
|
+
binding: Address;
|
|
18
|
+
commitmentSeq: string;
|
|
19
|
+
leaves: DisclosedLeaf[];
|
|
20
|
+
/** Reserved by contracts §9; not produced or checked by this version. */
|
|
21
|
+
ledger?: {
|
|
22
|
+
eventBytes: string;
|
|
23
|
+
subsequentHashes: string[];
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
/** Leaf value bytes, fixed per index (hashes.rs `leaf_values`). */
|
|
27
|
+
export declare function commitmentLeafValues(policy: CardPolicy, period: CardPeriod): Promise<Uint8Array[]>;
|
|
28
|
+
export declare function leafSalt(masterSalt: Uint8Array, index: number): Promise<Uint8Array>;
|
|
29
|
+
export declare function leafHash(index: number, salt: Uint8Array, value: Uint8Array): Promise<Uint8Array>;
|
|
30
|
+
export declare function nodeHash(left: Uint8Array, right: Uint8Array): Promise<Uint8Array>;
|
|
31
|
+
/** Root for the given policy, period and checkpoint salt (hashes.rs `commitment_root`). */
|
|
32
|
+
export declare function commitmentRoot(policy: CardPolicy, period: CardPeriod, masterSalt: Uint8Array): Promise<Uint8Array>;
|
|
33
|
+
/** Owner side: disclose only the picked leaves. `masterSalt` comes from the owner's encrypted recovery record. */
|
|
34
|
+
export declare function buildDisclosureBundle(input: {
|
|
35
|
+
policy: CardPolicy;
|
|
36
|
+
period: CardPeriod;
|
|
37
|
+
masterSalt: Uint8Array;
|
|
38
|
+
commitmentSeq: bigint;
|
|
39
|
+
indices: readonly number[];
|
|
40
|
+
}): Promise<DisclosureBundle>;
|
|
41
|
+
/** `/verify/card#disclose=<base64url(JSON)>`. The fragment never reaches a server. */
|
|
42
|
+
export declare function encodeDisclosureFragment(bundle: DisclosureBundle): string;
|
|
43
|
+
/** Strict parse: unknown shapes are rejected, never partially trusted. */
|
|
44
|
+
export declare function decodeDisclosureFragment(fragment: string): DisclosureBundle;
|
|
45
|
+
export type DisclosureLeafCheck = {
|
|
46
|
+
i: number;
|
|
47
|
+
ok: boolean;
|
|
48
|
+
field: CommitmentFieldView;
|
|
49
|
+
};
|
|
50
|
+
export type DisclosureCheck = {
|
|
51
|
+
state: "verified";
|
|
52
|
+
seq: string;
|
|
53
|
+
root: string;
|
|
54
|
+
writtenSlot: string;
|
|
55
|
+
leaves: DisclosureLeafCheck[];
|
|
56
|
+
} | {
|
|
57
|
+
state: "mismatch";
|
|
58
|
+
seq: string;
|
|
59
|
+
root: string;
|
|
60
|
+
writtenSlot: string;
|
|
61
|
+
leaves: DisclosureLeafCheck[];
|
|
62
|
+
} | {
|
|
63
|
+
state: "superseded";
|
|
64
|
+
bundleSeq: string;
|
|
65
|
+
chainSeq: string;
|
|
66
|
+
leaves: DisclosureLeafCheck[];
|
|
67
|
+
} | {
|
|
68
|
+
state: "wrong_card";
|
|
69
|
+
leaves: DisclosureLeafCheck[];
|
|
70
|
+
};
|
|
71
|
+
export declare function recomputeDisclosedRoot(leaf: DisclosedLeaf): Promise<Uint8Array>;
|
|
72
|
+
/**
|
|
73
|
+
* Check every disclosed field against the public commitment account. The
|
|
74
|
+
* commitment holds only its latest root, so a record from an older checkpoint
|
|
75
|
+
* is reported as superseded rather than guessed at.
|
|
76
|
+
*/
|
|
77
|
+
export declare function verifyDisclosureBundle(bundle: DisclosureBundle, commitment: CardCommitment): Promise<DisclosureCheck>;
|
|
78
|
+
export type CommitmentFieldView = {
|
|
79
|
+
i: number;
|
|
80
|
+
label: string;
|
|
81
|
+
value: string;
|
|
82
|
+
detail?: string;
|
|
83
|
+
};
|
|
84
|
+
export declare const COMMITMENT_FIELD_LABELS: Record<number, string>;
|
|
85
|
+
/** Human-readable value for one leaf. Exact cents; malformed bytes say so instead of guessing. */
|
|
86
|
+
export declare function describeCommitmentLeaf(i: number, value: Uint8Array): CommitmentFieldView;
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { PublicKey } from "@solana/web3.js";
|
|
2
|
+
import { formatFeeBps, formatUsdCents } from "./math.js";
|
|
3
|
+
/*
|
|
4
|
+
* Salted commitment leaves and single-field disclosure (contracts.md §1.6, §9).
|
|
5
|
+
*
|
|
6
|
+
* Byte layouts mirror programs/card_policy/src/hashes.rs exactly. The owner
|
|
7
|
+
* builds a disclosure bundle for the fields they pick; anyone recomputes the
|
|
8
|
+
* leaf and its Merkle path against the public CardCommitment root. This is an
|
|
9
|
+
* integrity check against ChainPay's on-chain commitment, not a zero-knowledge
|
|
10
|
+
* proof: the disclosed values are shown in the clear.
|
|
11
|
+
*/
|
|
12
|
+
export const COMMITMENT_LEAF_COUNT = 16;
|
|
13
|
+
const LEAF_DOMAIN = "chainpay-card-leaf:v1\n";
|
|
14
|
+
const DISCLOSURE_PARAM = "disclose";
|
|
15
|
+
// --------------------------------------------------------------- bytes
|
|
16
|
+
const enc = new TextEncoder();
|
|
17
|
+
async function sha256(...parts) {
|
|
18
|
+
const total = parts.reduce((sum, part) => sum + part.length, 0);
|
|
19
|
+
const joined = new Uint8Array(total);
|
|
20
|
+
let offset = 0;
|
|
21
|
+
for (const part of parts) {
|
|
22
|
+
joined.set(part, offset);
|
|
23
|
+
offset += part.length;
|
|
24
|
+
}
|
|
25
|
+
return new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", joined.buffer));
|
|
26
|
+
}
|
|
27
|
+
function bytesToHex(bytes) {
|
|
28
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
29
|
+
}
|
|
30
|
+
function hexToBytes(value) {
|
|
31
|
+
if (typeof value !== "string" || value.length % 2 !== 0 || !/^[0-9a-f]*$/.test(value))
|
|
32
|
+
throw new Error("Expected lowercase hex");
|
|
33
|
+
return Uint8Array.from(value.match(/../g) ?? [], (pair) => parseInt(pair, 16));
|
|
34
|
+
}
|
|
35
|
+
function le(value, bytes, signed = false) {
|
|
36
|
+
const out = new Uint8Array(bytes);
|
|
37
|
+
const view = new DataView(out.buffer);
|
|
38
|
+
if (bytes === 2)
|
|
39
|
+
view.setUint16(0, Number(value), true);
|
|
40
|
+
else if (bytes === 4)
|
|
41
|
+
view.setUint32(0, Number(value), true);
|
|
42
|
+
else if (signed)
|
|
43
|
+
view.setBigInt64(0, value, true);
|
|
44
|
+
else
|
|
45
|
+
view.setBigUint64(0, value, true);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
function cat(...parts) {
|
|
49
|
+
const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
|
|
50
|
+
let offset = 0;
|
|
51
|
+
for (const part of parts) {
|
|
52
|
+
out.set(part, offset);
|
|
53
|
+
offset += part.length;
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
function compareBytes(a, b) {
|
|
58
|
+
for (let i = 0; i < Math.min(a.length, b.length); i += 1)
|
|
59
|
+
if (a[i] !== b[i])
|
|
60
|
+
return a[i] - b[i];
|
|
61
|
+
return a.length - b.length;
|
|
62
|
+
}
|
|
63
|
+
const RECOVERY_BYTE = { normal: 0, recovery_frozen: 1, restored_pending_reconcile: 2 };
|
|
64
|
+
// --------------------------------------------------------------- leaves
|
|
65
|
+
/** Leaf value bytes, fixed per index (hashes.rs `leaf_values`). */
|
|
66
|
+
export async function commitmentLeafValues(policy, period) {
|
|
67
|
+
const merchants = [...policy.merchantIdHashes].sort(compareBytes);
|
|
68
|
+
const mccs = [...policy.mccs].sort((a, b) => a - b);
|
|
69
|
+
return [
|
|
70
|
+
new PublicKey(policy.binding).toBytes(),
|
|
71
|
+
le(BigInt(policy.policyVersion), 4),
|
|
72
|
+
le(policy.budgetCents, 8),
|
|
73
|
+
le(policy.maxPurchaseCents, 8),
|
|
74
|
+
le(BigInt(policy.maxPurchasesPerPeriod), 2),
|
|
75
|
+
await sha256(...merchants),
|
|
76
|
+
await sha256(cat(...mccs.map((mcc) => le(BigInt(mcc), 2)))),
|
|
77
|
+
cat(le(policy.expiresAt, 8, true), Uint8Array.of(policy.recurringAllowed ? 1 : 0), le(BigInt(policy.feeBps), 2)),
|
|
78
|
+
cat(le(BigInt(period.periodIndex), 4), le(period.periodStart, 8, true), le(period.periodEnd, 8, true)),
|
|
79
|
+
le(period.capturedCents, 8),
|
|
80
|
+
le(period.reservedCents, 8),
|
|
81
|
+
le(period.refundedCents, 8),
|
|
82
|
+
le(BigInt(period.purchasesCount), 2),
|
|
83
|
+
le(policy.statementOutstandingCents, 8),
|
|
84
|
+
Uint8Array.of(policy.frozen ? 1 : 0, RECOVERY_BYTE[policy.recoveryState]),
|
|
85
|
+
cat(policy.ledgerHead, le(policy.ledgerSeq, 8)),
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
export function leafSalt(masterSalt, index) {
|
|
89
|
+
if (masterSalt.length !== 32)
|
|
90
|
+
throw new Error("master salt must be 32 bytes");
|
|
91
|
+
return sha256(masterSalt, Uint8Array.of(index));
|
|
92
|
+
}
|
|
93
|
+
export function leafHash(index, salt, value) {
|
|
94
|
+
return sha256(enc.encode(LEAF_DOMAIN), Uint8Array.of(index), salt, value);
|
|
95
|
+
}
|
|
96
|
+
export function nodeHash(left, right) {
|
|
97
|
+
return sha256(Uint8Array.of(1), left, right);
|
|
98
|
+
}
|
|
99
|
+
async function levels(leaves) {
|
|
100
|
+
if (leaves.length !== COMMITMENT_LEAF_COUNT)
|
|
101
|
+
throw new Error(`a commitment has exactly ${COMMITMENT_LEAF_COUNT} leaves`);
|
|
102
|
+
const out = [leaves];
|
|
103
|
+
while (out[out.length - 1].length > 1) {
|
|
104
|
+
const level = out[out.length - 1];
|
|
105
|
+
const next = [];
|
|
106
|
+
for (let i = 0; i < level.length; i += 2)
|
|
107
|
+
next.push(await nodeHash(level[i], level[i + 1]));
|
|
108
|
+
out.push(next);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
/** Root for the given policy, period and checkpoint salt (hashes.rs `commitment_root`). */
|
|
113
|
+
export async function commitmentRoot(policy, period, masterSalt) {
|
|
114
|
+
const values = await commitmentLeafValues(policy, period);
|
|
115
|
+
const leaves = await Promise.all(values.map(async (value, i) => leafHash(i, await leafSalt(masterSalt, i), value)));
|
|
116
|
+
const tree = await levels(leaves);
|
|
117
|
+
return tree[tree.length - 1][0];
|
|
118
|
+
}
|
|
119
|
+
/** Owner side: disclose only the picked leaves. `masterSalt` comes from the owner's encrypted recovery record. */
|
|
120
|
+
export async function buildDisclosureBundle(input) {
|
|
121
|
+
const indices = [...new Set(input.indices)].sort((a, b) => a - b);
|
|
122
|
+
if (indices.length === 0)
|
|
123
|
+
throw new Error("Pick at least one field to share");
|
|
124
|
+
if (indices.some((i) => !Number.isInteger(i) || i < 0 || i >= COMMITMENT_LEAF_COUNT))
|
|
125
|
+
throw new Error("Unknown field");
|
|
126
|
+
const values = await commitmentLeafValues(input.policy, input.period);
|
|
127
|
+
const salts = await Promise.all(values.map((_, i) => leafSalt(input.masterSalt, i)));
|
|
128
|
+
const leaves = await Promise.all(values.map((value, i) => leafHash(i, salts[i], value)));
|
|
129
|
+
const tree = await levels(leaves);
|
|
130
|
+
return {
|
|
131
|
+
v: 1,
|
|
132
|
+
binding: new PublicKey(input.policy.binding).toBase58(),
|
|
133
|
+
commitmentSeq: input.commitmentSeq.toString(),
|
|
134
|
+
leaves: indices.map((i) => {
|
|
135
|
+
const proof = [];
|
|
136
|
+
let idx = i;
|
|
137
|
+
for (let depth = 0; depth < tree.length - 1; depth += 1) {
|
|
138
|
+
proof.push(bytesToHex(tree[depth][idx ^ 1]));
|
|
139
|
+
idx >>= 1;
|
|
140
|
+
}
|
|
141
|
+
return { i, value: bytesToHex(values[i]), leafSalt: bytesToHex(salts[i]), proof };
|
|
142
|
+
}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// --------------------------------------------------------------- fragment
|
|
146
|
+
function toBase64Url(text) {
|
|
147
|
+
let binary = "";
|
|
148
|
+
for (const byte of enc.encode(text))
|
|
149
|
+
binary += String.fromCharCode(byte);
|
|
150
|
+
return globalThis.btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
151
|
+
}
|
|
152
|
+
function fromBase64Url(value) {
|
|
153
|
+
const binary = globalThis.atob(value.replace(/-/g, "+").replace(/_/g, "/"));
|
|
154
|
+
return new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));
|
|
155
|
+
}
|
|
156
|
+
/** `/verify/card#disclose=<base64url(JSON)>`. The fragment never reaches a server. */
|
|
157
|
+
export function encodeDisclosureFragment(bundle) {
|
|
158
|
+
return `${DISCLOSURE_PARAM}=${toBase64Url(JSON.stringify(bundle))}`;
|
|
159
|
+
}
|
|
160
|
+
/** Strict parse: unknown shapes are rejected, never partially trusted. */
|
|
161
|
+
export function decodeDisclosureFragment(fragment) {
|
|
162
|
+
const match = fragment.match(/(?:^#?|&)disclose=([A-Za-z0-9_-]+)/);
|
|
163
|
+
if (!match)
|
|
164
|
+
throw new Error("No shared card record in this link");
|
|
165
|
+
let raw;
|
|
166
|
+
try {
|
|
167
|
+
raw = JSON.parse(fromBase64Url(match[1]));
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
throw new Error("The shared card record in this link can't be read");
|
|
171
|
+
}
|
|
172
|
+
if (raw.v !== 1)
|
|
173
|
+
throw new Error("Unsupported card record version");
|
|
174
|
+
if (typeof raw.binding !== "string")
|
|
175
|
+
throw new Error("The card record has no card link");
|
|
176
|
+
new PublicKey(raw.binding);
|
|
177
|
+
if (typeof raw.commitmentSeq !== "string" || !/^(0|[1-9][0-9]{0,19})$/.test(raw.commitmentSeq))
|
|
178
|
+
throw new Error("The card record has no commitment number");
|
|
179
|
+
if (!Array.isArray(raw.leaves) || raw.leaves.length === 0 || raw.leaves.length > COMMITMENT_LEAF_COUNT)
|
|
180
|
+
throw new Error("The card record has no fields");
|
|
181
|
+
const seen = new Set();
|
|
182
|
+
const leaves = raw.leaves.map((leaf) => {
|
|
183
|
+
if (!leaf || !Number.isInteger(leaf.i) || leaf.i < 0 || leaf.i >= COMMITMENT_LEAF_COUNT || seen.has(leaf.i))
|
|
184
|
+
throw new Error("The card record has an unknown field");
|
|
185
|
+
seen.add(leaf.i);
|
|
186
|
+
hexToBytes(leaf.value);
|
|
187
|
+
if (hexToBytes(leaf.leafSalt).length !== 32)
|
|
188
|
+
throw new Error("A field's salt has the wrong length");
|
|
189
|
+
if (!Array.isArray(leaf.proof) || leaf.proof.length !== 4 || leaf.proof.some((node) => typeof node !== "string" || hexToBytes(node).length !== 32))
|
|
190
|
+
throw new Error("A field's proof has the wrong shape");
|
|
191
|
+
return { i: leaf.i, value: leaf.value, leafSalt: leaf.leafSalt, proof: [...leaf.proof] };
|
|
192
|
+
});
|
|
193
|
+
return { v: 1, binding: raw.binding, commitmentSeq: raw.commitmentSeq, leaves };
|
|
194
|
+
}
|
|
195
|
+
export async function recomputeDisclosedRoot(leaf) {
|
|
196
|
+
let acc = await leafHash(leaf.i, hexToBytes(leaf.leafSalt), hexToBytes(leaf.value));
|
|
197
|
+
let idx = leaf.i;
|
|
198
|
+
for (const sibling of leaf.proof) {
|
|
199
|
+
const node = hexToBytes(sibling);
|
|
200
|
+
acc = idx % 2 === 0 ? await nodeHash(acc, node) : await nodeHash(node, acc);
|
|
201
|
+
idx >>= 1;
|
|
202
|
+
}
|
|
203
|
+
return acc;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Check every disclosed field against the public commitment account. The
|
|
207
|
+
* commitment holds only its latest root, so a record from an older checkpoint
|
|
208
|
+
* is reported as superseded rather than guessed at.
|
|
209
|
+
*/
|
|
210
|
+
export async function verifyDisclosureBundle(bundle, commitment) {
|
|
211
|
+
const described = bundle.leaves.map((leaf) => ({ i: leaf.i, ok: false, field: describeCommitmentLeaf(leaf.i, hexToBytes(leaf.value)) }));
|
|
212
|
+
if (new PublicKey(commitment.binding).toBase58() !== new PublicKey(bundle.binding).toBase58())
|
|
213
|
+
return { state: "wrong_card", leaves: described };
|
|
214
|
+
const bindingLeaf = bundle.leaves.find((leaf) => leaf.i === 0);
|
|
215
|
+
if (bindingLeaf && bindingLeaf.value !== bytesToHex(new PublicKey(bundle.binding).toBytes()))
|
|
216
|
+
return { state: "wrong_card", leaves: described };
|
|
217
|
+
if (commitment.seq.toString() !== bundle.commitmentSeq) {
|
|
218
|
+
return { state: "superseded", bundleSeq: bundle.commitmentSeq, chainSeq: commitment.seq.toString(), leaves: described };
|
|
219
|
+
}
|
|
220
|
+
const root = bytesToHex(commitment.root);
|
|
221
|
+
const leaves = await Promise.all(bundle.leaves.map(async (leaf, n) => ({ ...described[n], ok: bytesToHex(await recomputeDisclosedRoot(leaf)) === root })));
|
|
222
|
+
const base = { seq: commitment.seq.toString(), root, writtenSlot: commitment.writtenSlot.toString(), leaves };
|
|
223
|
+
return leaves.every((leaf) => leaf.ok) ? { state: "verified", ...base } : { state: "mismatch", ...base };
|
|
224
|
+
}
|
|
225
|
+
export const COMMITMENT_FIELD_LABELS = {
|
|
226
|
+
0: "Card",
|
|
227
|
+
1: "Rules version",
|
|
228
|
+
2: "Budget per period",
|
|
229
|
+
3: "Max per purchase",
|
|
230
|
+
4: "Purchases allowed per period",
|
|
231
|
+
5: "Shop list fingerprint",
|
|
232
|
+
6: "Category list fingerprint",
|
|
233
|
+
7: "End date and fee",
|
|
234
|
+
8: "Period",
|
|
235
|
+
9: "Charged this period",
|
|
236
|
+
10: "Held right now",
|
|
237
|
+
11: "Refunded this period",
|
|
238
|
+
12: "Purchases this period",
|
|
239
|
+
13: "Owed on the statement",
|
|
240
|
+
14: "Frozen",
|
|
241
|
+
15: "Activity log position",
|
|
242
|
+
};
|
|
243
|
+
function readLe(bytes, offset, size, signed = false) {
|
|
244
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
245
|
+
if (size === 2)
|
|
246
|
+
return BigInt(view.getUint16(offset, true));
|
|
247
|
+
if (size === 4)
|
|
248
|
+
return BigInt(view.getUint32(offset, true));
|
|
249
|
+
return signed ? view.getBigInt64(offset, true) : view.getBigUint64(offset, true);
|
|
250
|
+
}
|
|
251
|
+
function dateFromSeconds(seconds) {
|
|
252
|
+
const ms = Number(seconds) * 1000;
|
|
253
|
+
return Number.isFinite(ms) && Math.abs(ms) <= 8.64e15 ? new Date(ms).toISOString().slice(0, 10) : `unix ${seconds}`;
|
|
254
|
+
}
|
|
255
|
+
const EXPECTED_LENGTH = { 0: 32, 1: 4, 2: 8, 3: 8, 4: 2, 5: 32, 6: 32, 7: 11, 8: 20, 9: 8, 10: 8, 11: 8, 12: 2, 13: 8, 14: 2, 15: 40 };
|
|
256
|
+
/** Human-readable value for one leaf. Exact cents; malformed bytes say so instead of guessing. */
|
|
257
|
+
export function describeCommitmentLeaf(i, value) {
|
|
258
|
+
const label = COMMITMENT_FIELD_LABELS[i] ?? `Field ${i}`;
|
|
259
|
+
if (value.length !== EXPECTED_LENGTH[i])
|
|
260
|
+
return { i, label, value: "Unreadable value" };
|
|
261
|
+
const cents = (offset = 0) => formatUsdCents(readLe(value, offset, 8));
|
|
262
|
+
switch (i) {
|
|
263
|
+
case 0: return { i, label, value: new PublicKey(value).toBase58() };
|
|
264
|
+
case 1: return { i, label, value: `Version ${readLe(value, 0, 4)}` };
|
|
265
|
+
case 2:
|
|
266
|
+
case 3:
|
|
267
|
+
case 9:
|
|
268
|
+
case 10:
|
|
269
|
+
case 11:
|
|
270
|
+
case 13: return { i, label, value: cents() };
|
|
271
|
+
case 4: {
|
|
272
|
+
const n = readLe(value, 0, 2);
|
|
273
|
+
return { i, label, value: n === 0n ? "No count limit" : n.toString() };
|
|
274
|
+
}
|
|
275
|
+
case 12: return { i, label, value: readLe(value, 0, 2).toString() };
|
|
276
|
+
case 5:
|
|
277
|
+
case 6: return { i, label, value: bytesToHex(value).slice(0, 16), detail: bytesToHex(value) };
|
|
278
|
+
case 7: {
|
|
279
|
+
const end = readLe(value, 0, 8, true);
|
|
280
|
+
const fee = Number(readLe(value, 9, 2));
|
|
281
|
+
return { i, label, value: `${end === 0n ? "No end date" : `Ends ${dateFromSeconds(end)}`} · fee ${fee <= 1000 ? formatFeeBps(fee) : `${fee} bps`}`, detail: value[8] ? "Repeat charges allowed" : "No repeat charges" };
|
|
282
|
+
}
|
|
283
|
+
case 8: return { i, label, value: `Period ${readLe(value, 0, 4)}`, detail: `${dateFromSeconds(readLe(value, 4, 8, true))} to ${dateFromSeconds(readLe(value, 12, 8, true))}` };
|
|
284
|
+
case 14: return { i, label, value: value[0] ? "Frozen" : "Not frozen", detail: ["Normal", "Frozen for recovery", "Restored, waiting for confirmation"][value[1]] ?? "Unknown recovery state" };
|
|
285
|
+
case 15: return { i, label, value: `Event ${readLe(value, 32, 8)}`, detail: bytesToHex(value.slice(0, 32)) };
|
|
286
|
+
default: return { i, label, value: bytesToHex(value) };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { Address } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* card_policy program ID on Devnet (contracts.md Changelog, Lane 1 / 1A;
|
|
4
|
+
* `address` in programs/card_policy/idl/card_policy.json). Every builder and
|
|
5
|
+
* PDA helper takes an explicit `programId` and falls back to this value.
|
|
6
|
+
*/
|
|
7
|
+
export declare const CARD_POLICY_PROGRAM_ID: Address;
|
|
8
|
+
export declare function resolveCardPolicyProgramId(programId?: Address): Address;
|
|
9
|
+
/** MagicBlock Devnet TEE (Private Ephemeral Rollup). Tokens go in the query string; never log the full URL. */
|
|
10
|
+
export declare const DEVNET_TEE_URL = "https://devnet-tee.magicblock.app";
|
|
11
|
+
export declare const DEVNET_TEE_WS_URL = "wss://devnet-tee.magicblock.app";
|
|
12
|
+
/** The only validator `delegate_card` accepts on Devnet (contracts.md §1). */
|
|
13
|
+
export declare const DEVNET_TEE_VALIDATOR: Address;
|
|
14
|
+
export declare const TEE_VALIDATOR_ALLOWLIST: readonly Address[];
|
|
15
|
+
export declare const DELEGATION_PROGRAM_ID: Address;
|
|
16
|
+
export declare const MAGIC_PROGRAM_ID: Address;
|
|
17
|
+
export declare const MAGIC_CONTEXT_ID: Address;
|
|
18
|
+
export declare const PERMISSION_PROGRAM_ID: Address;
|
|
19
|
+
export declare const EPHEMERAL_VAULT_ID: Address;
|
|
20
|
+
export declare const CARD_SEEDS: {
|
|
21
|
+
readonly binding: "card_binding";
|
|
22
|
+
readonly policy: "card_policy";
|
|
23
|
+
readonly period: "card_period";
|
|
24
|
+
readonly reservation: "res";
|
|
25
|
+
readonly intent: "intent";
|
|
26
|
+
readonly commitment: "card_commit";
|
|
27
|
+
/** Per-card replay guard for closed reservations (PER only). */
|
|
28
|
+
readonly authGuard: "auth_guard";
|
|
29
|
+
/** Lamport-only base PDA: the `approved_agent` of the owner's repayment mandate. */
|
|
30
|
+
readonly repayAgent: "repay_agent";
|
|
31
|
+
};
|
|
32
|
+
export declare const MAX_MERCHANTS = 8;
|
|
33
|
+
export declare const MAX_MCCS = 16;
|
|
34
|
+
/** owner + authorizer + 4 finance readers. */
|
|
35
|
+
export declare const MAX_MEMBERS = 6;
|
|
36
|
+
/** $10,000 sandbox cap on any budget (contracts.md §1.3 #6). */
|
|
37
|
+
export declare const MAX_BUDGET_CENTS = 1000000n;
|
|
38
|
+
export declare const MIN_PERIOD_SECONDS = 86400;
|
|
39
|
+
export declare const MAX_FEE_BPS = 1000;
|
|
40
|
+
/** A checkout intent may live at most 10 minutes. */
|
|
41
|
+
export declare const MAX_INTENT_TTL_SECONDS = 600;
|
|
42
|
+
export declare const CARD_ISSUER: {
|
|
43
|
+
readonly lithic_sandbox: 1;
|
|
44
|
+
readonly card_sim: 2;
|
|
45
|
+
};
|
|
46
|
+
/** EphemeralPermission member flags used by card_policy (MagicBlock permission program). */
|
|
47
|
+
export declare const PERMISSION_FLAGS: {
|
|
48
|
+
readonly AUTHORITY: 1;
|
|
49
|
+
readonly TX_LOGS: 2;
|
|
50
|
+
readonly TX_BALANCES: 4;
|
|
51
|
+
readonly TX_MESSAGE: 8;
|
|
52
|
+
};
|
|
53
|
+
/** Anchor instruction discriminators: sha256("global:<name>")[0..8]. */
|
|
54
|
+
export declare const CARD_POLICY_DISCRIMINATORS: {
|
|
55
|
+
readonly initCard: Uint8Array<ArrayBuffer>;
|
|
56
|
+
readonly delegateCard: Uint8Array<ArrayBuffer>;
|
|
57
|
+
readonly initPermission: Uint8Array<ArrayBuffer>;
|
|
58
|
+
readonly updatePermission: Uint8Array<ArrayBuffer>;
|
|
59
|
+
readonly syncPermission: Uint8Array<ArrayBuffer>;
|
|
60
|
+
readonly setPolicy: Uint8Array<ArrayBuffer>;
|
|
61
|
+
readonly openCheckoutIntent: Uint8Array<ArrayBuffer>;
|
|
62
|
+
readonly cancelCheckoutIntent: Uint8Array<ArrayBuffer>;
|
|
63
|
+
readonly closeCheckoutIntent: Uint8Array<ArrayBuffer>;
|
|
64
|
+
readonly authorize: Uint8Array<ArrayBuffer>;
|
|
65
|
+
readonly adjustReservation: Uint8Array<ArrayBuffer>;
|
|
66
|
+
readonly capture: Uint8Array<ArrayBuffer>;
|
|
67
|
+
readonly reverse: Uint8Array<ArrayBuffer>;
|
|
68
|
+
readonly refund: Uint8Array<ArrayBuffer>;
|
|
69
|
+
readonly recordDispute: Uint8Array<ArrayBuffer>;
|
|
70
|
+
readonly recordException: Uint8Array<ArrayBuffer>;
|
|
71
|
+
readonly resolveException: Uint8Array<ArrayBuffer>;
|
|
72
|
+
readonly rollPeriod: Uint8Array<ArrayBuffer>;
|
|
73
|
+
readonly freeze: Uint8Array<ArrayBuffer>;
|
|
74
|
+
readonly unfreeze: Uint8Array<ArrayBuffer>;
|
|
75
|
+
readonly recoveryFreeze: Uint8Array<ArrayBuffer>;
|
|
76
|
+
readonly restore: Uint8Array<ArrayBuffer>;
|
|
77
|
+
readonly confirmReconciled: Uint8Array<ArrayBuffer>;
|
|
78
|
+
readonly checkpoint: Uint8Array<ArrayBuffer>;
|
|
79
|
+
readonly writeCommitment: Uint8Array<ArrayBuffer>;
|
|
80
|
+
readonly wipeCard: Uint8Array<ArrayBuffer>;
|
|
81
|
+
readonly closeCard: Uint8Array<ArrayBuffer>;
|
|
82
|
+
readonly recordRepayment: Uint8Array<ArrayBuffer>;
|
|
83
|
+
readonly closeReservation: Uint8Array<ArrayBuffer>;
|
|
84
|
+
/** Base layer: the card's repay agent pays a statement by ChainPay `execute_payment` CPI. */
|
|
85
|
+
readonly repayStatement: Uint8Array<ArrayBuffer>;
|
|
86
|
+
/** PER: MagicBlock private payment, authorizer-attested (no ChainPay receipt). */
|
|
87
|
+
readonly recordPrivateRepayment: Uint8Array<ArrayBuffer>;
|
|
88
|
+
};
|
|
89
|
+
export type CardPolicyInstructionName = keyof typeof CARD_POLICY_DISCRIMINATORS;
|
|
90
|
+
/** Anchor account discriminators: sha256("account:<Name>")[0..8]. */
|
|
91
|
+
export declare const CARD_ACCOUNT_DISCRIMINATORS: {
|
|
92
|
+
readonly cardBinding: Uint8Array<ArrayBuffer>;
|
|
93
|
+
readonly cardPolicy: Uint8Array<ArrayBuffer>;
|
|
94
|
+
readonly cardPeriod: Uint8Array<ArrayBuffer>;
|
|
95
|
+
readonly reservation: Uint8Array<ArrayBuffer>;
|
|
96
|
+
readonly checkoutIntent: Uint8Array<ArrayBuffer>;
|
|
97
|
+
readonly cardCommitment: Uint8Array<ArrayBuffer>;
|
|
98
|
+
/** Zero-copy, 8 KB: the last 256 closed reservations' auth ids. */
|
|
99
|
+
readonly authGuard: Uint8Array<ArrayBuffer>;
|
|
100
|
+
};
|
|
101
|
+
/** `CardPolicyError` codes (contracts.md §1.4). Append only. */
|
|
102
|
+
export declare const CARD_POLICY_ERRORS: {
|
|
103
|
+
readonly 6000: "Unauthorized";
|
|
104
|
+
readonly 6001: "ValidatorNotAllowed";
|
|
105
|
+
readonly 6002: "PolicyNotSet";
|
|
106
|
+
readonly 6003: "InvalidPolicy";
|
|
107
|
+
readonly 6004: "CardFrozen";
|
|
108
|
+
readonly 6005: "RecoveryFrozen";
|
|
109
|
+
readonly 6006: "PolicyExpired";
|
|
110
|
+
readonly 6007: "IntentInvalid";
|
|
111
|
+
readonly 6008: "IntentExpired";
|
|
112
|
+
readonly 6009: "IntentStale";
|
|
113
|
+
readonly 6010: "MerchantMismatch";
|
|
114
|
+
readonly 6011: "MerchantNotAllowed";
|
|
115
|
+
readonly 6012: "MccNotAllowed";
|
|
116
|
+
readonly 6013: "CurrencyMismatch";
|
|
117
|
+
readonly 6014: "AmountExceedsIntent";
|
|
118
|
+
readonly 6015: "AmountExceedsMax";
|
|
119
|
+
readonly 6016: "BudgetExceeded";
|
|
120
|
+
readonly 6017: "VelocityExceeded";
|
|
121
|
+
readonly 6018: "RecurringNotAllowed";
|
|
122
|
+
readonly 6019: "DuplicateAuthorization";
|
|
123
|
+
readonly 6020: "DuplicateCapture";
|
|
124
|
+
readonly 6021: "ReservationClosed";
|
|
125
|
+
readonly 6022: "PeriodNotEnded";
|
|
126
|
+
readonly 6023: "DisclosureBlocked";
|
|
127
|
+
readonly 6024: "MemberLimit";
|
|
128
|
+
readonly 6025: "ExceptionsOpen";
|
|
129
|
+
readonly 6026: "NotInRecovery";
|
|
130
|
+
readonly 6027: "StaleCommitment";
|
|
131
|
+
readonly 6028: "OpenReservations";
|
|
132
|
+
readonly 6029: "OutstandingBalance";
|
|
133
|
+
readonly 6030: "MathOverflow";
|
|
134
|
+
readonly 6031: "AuthorizerChangeRequiresFreeze";
|
|
135
|
+
readonly 6032: "DuplicateRepayment";
|
|
136
|
+
readonly 6033: "MemberNotFound";
|
|
137
|
+
readonly 6034: "PermissionNotInitialized";
|
|
138
|
+
readonly 6035: "InvalidAmount";
|
|
139
|
+
readonly 6036: "NoOpenExceptions";
|
|
140
|
+
readonly 6037: "ReconDigestMismatch";
|
|
141
|
+
readonly 6038: "RepaymentExceedsOutstanding";
|
|
142
|
+
readonly 6039: "InvalidAccount";
|
|
143
|
+
readonly 6040: "PrefundTooLow";
|
|
144
|
+
readonly 6041: "NotWiped";
|
|
145
|
+
readonly 6042: "InvalidEventId";
|
|
146
|
+
readonly 6043: "DuplicateEvent";
|
|
147
|
+
readonly 6044: "EphemeralAccountsOpen";
|
|
148
|
+
readonly 6045: "ReservationNotFinal";
|
|
149
|
+
readonly 6046: "CoSignerRequired";
|
|
150
|
+
readonly 6047: "RefundExceedsCapture";
|
|
151
|
+
readonly 6048: "CaptureLimit";
|
|
152
|
+
readonly 6049: "BudgetBelowCommitted";
|
|
153
|
+
readonly 6050: "InvalidRepaymentReceipt";
|
|
154
|
+
readonly 6051: "RepaymentRecipientMismatch";
|
|
155
|
+
readonly 6052: "RepaymentExceedsReceipt";
|
|
156
|
+
readonly 6053: "InvalidRepaymentMandate";
|
|
157
|
+
};
|
|
158
|
+
export type CardPolicyErrorCode = keyof typeof CARD_POLICY_ERRORS;
|
|
159
|
+
export type CardPolicyErrorName = (typeof CARD_POLICY_ERRORS)[CardPolicyErrorCode];
|
|
160
|
+
export declare function cardPolicyErrorName(code: number): CardPolicyErrorName | undefined;
|