@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
package/dist/mandate.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Keypair, PublicKey } from "@solana/web3.js";
|
|
2
|
+
import { DEFAULT_PROGRAM_ID, MANDATE_NONCE_PREFIX } from "./constants.js";
|
|
3
|
+
import { encodeApproveChecked, encodeCreateMandate, encodeInitializeConfig, encodePauseMandate, encodeRevokeDelegate, encodeRevokeMandate, encodeRegisterAsset, encodeSetAssetStatus, encodeUpdateMandate, instruction, isMandateNonce, meta, publicKey, systemProgramMeta, tokenProgramAddress, writeU64, } from "./encoding.js";
|
|
4
|
+
import { deriveAssetAddress, deriveConfigAddress, deriveMandateAddress, deriveVersionedMandateAddress, } from "./pda.js";
|
|
5
|
+
export function createMandateNonce() {
|
|
6
|
+
const bytes = Keypair.generate().publicKey.toBytes();
|
|
7
|
+
bytes.set(MANDATE_NONCE_PREFIX, 0);
|
|
8
|
+
return new PublicKey(bytes).toBase58();
|
|
9
|
+
}
|
|
10
|
+
function normalizedMandateNonce(input) {
|
|
11
|
+
const mandateNonce = input.mandateNonce ?? createMandateNonce();
|
|
12
|
+
publicKey(mandateNonce);
|
|
13
|
+
if (!isMandateNonce(mandateNonce)) {
|
|
14
|
+
throw new Error("mandateNonce must be generated by ChainPay and include its nonce prefix");
|
|
15
|
+
}
|
|
16
|
+
return mandateNonce;
|
|
17
|
+
}
|
|
18
|
+
export function buildInitializeConfigInstruction(supportedMints, authority, programId = DEFAULT_PROGRAM_ID) {
|
|
19
|
+
if (supportedMints.length !== 3) {
|
|
20
|
+
throw new Error("initialize_config requires exactly three mint slots");
|
|
21
|
+
}
|
|
22
|
+
publicKey(authority);
|
|
23
|
+
supportedMints.forEach((mint) => publicKey(mint));
|
|
24
|
+
return instruction("initialize_config", programId, [
|
|
25
|
+
meta(deriveConfigAddress(programId), true),
|
|
26
|
+
meta(authority, true, true),
|
|
27
|
+
systemProgramMeta(),
|
|
28
|
+
], encodeInitializeConfig(supportedMints));
|
|
29
|
+
}
|
|
30
|
+
export function validateMandateInput(input) {
|
|
31
|
+
publicKey(input.approvedAgent);
|
|
32
|
+
publicKey(input.sourceTokenAccount);
|
|
33
|
+
publicKey(input.allowedMint);
|
|
34
|
+
normalizedMandateNonce(input);
|
|
35
|
+
writeU64(input.maxPerPayment, "maxPerPayment");
|
|
36
|
+
writeU64(input.totalLimit, "totalLimit");
|
|
37
|
+
writeU64(input.expiresAtSlot, "expiresAtSlot");
|
|
38
|
+
writeU64(input.maxPaymentCount, "maxPaymentCount");
|
|
39
|
+
writeU64(input.cooldownSlots, "cooldownSlots");
|
|
40
|
+
if (input.maxPerPayment <= 0n) {
|
|
41
|
+
throw new Error("maxPerPayment must be positive");
|
|
42
|
+
}
|
|
43
|
+
if (input.totalLimit < input.maxPerPayment) {
|
|
44
|
+
throw new Error("totalLimit must cover maxPerPayment");
|
|
45
|
+
}
|
|
46
|
+
if (input.expiresAtSlot <= 0n) {
|
|
47
|
+
throw new Error("expiresAtSlot must be positive");
|
|
48
|
+
}
|
|
49
|
+
if (input.delegateAmount !== undefined) {
|
|
50
|
+
writeU64(input.delegateAmount, "delegateAmount");
|
|
51
|
+
if (input.delegateAmount <= 0n)
|
|
52
|
+
throw new Error("delegateAmount must be positive");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function buildCreateMandateInstruction(input, owner, programId = DEFAULT_PROGRAM_ID) {
|
|
56
|
+
const mandateNonce = normalizedMandateNonce(input);
|
|
57
|
+
const normalizedInput = { ...input, mandateNonce };
|
|
58
|
+
validateMandateInput(normalizedInput);
|
|
59
|
+
const config = deriveConfigAddress(programId);
|
|
60
|
+
const asset = deriveAssetAddress(normalizedInput.allowedMint, programId);
|
|
61
|
+
const mandate = deriveVersionedMandateAddress(owner, normalizedInput.allowedMint, mandateNonce, programId);
|
|
62
|
+
const tokenProgram = tokenProgramAddress(normalizedInput.tokenProgram);
|
|
63
|
+
return instruction("create_mandate", programId, [
|
|
64
|
+
meta(config),
|
|
65
|
+
meta(asset),
|
|
66
|
+
meta(mandate, true),
|
|
67
|
+
meta(owner, true, true),
|
|
68
|
+
meta(normalizedInput.allowedMint),
|
|
69
|
+
meta(normalizedInput.sourceTokenAccount),
|
|
70
|
+
meta(tokenProgram),
|
|
71
|
+
systemProgramMeta(),
|
|
72
|
+
], encodeCreateMandate(normalizedInput));
|
|
73
|
+
}
|
|
74
|
+
export function buildRegisterAssetInstruction(input, authority, programId = DEFAULT_PROGRAM_ID) {
|
|
75
|
+
publicKey(input.mint);
|
|
76
|
+
publicKey(authority);
|
|
77
|
+
return instruction("register_asset", programId, [
|
|
78
|
+
meta(deriveConfigAddress(programId)),
|
|
79
|
+
meta(deriveAssetAddress(input.mint, programId), true),
|
|
80
|
+
meta(authority, false, true),
|
|
81
|
+
meta(input.mint),
|
|
82
|
+
meta(tokenProgramAddress(input.tokenProgram)),
|
|
83
|
+
systemProgramMeta(),
|
|
84
|
+
], encodeRegisterAsset(input.mint));
|
|
85
|
+
}
|
|
86
|
+
export function buildSetAssetStatusInstruction(mint, authority, enabled, programId = DEFAULT_PROGRAM_ID) {
|
|
87
|
+
publicKey(mint);
|
|
88
|
+
publicKey(authority);
|
|
89
|
+
return instruction("set_asset_status", programId, [
|
|
90
|
+
meta(deriveConfigAddress(programId)),
|
|
91
|
+
meta(deriveAssetAddress(mint, programId), true),
|
|
92
|
+
meta(authority, false, true),
|
|
93
|
+
], encodeSetAssetStatus(enabled));
|
|
94
|
+
}
|
|
95
|
+
export function buildApproveDelegateInstruction(input) {
|
|
96
|
+
const mandate = input.mandate ?? deriveMandateAddress(input.owner);
|
|
97
|
+
const amount = input.delegateAmount ?? input.totalLimit;
|
|
98
|
+
writeU64(amount, "delegateAmount");
|
|
99
|
+
return instruction("approve_delegate", tokenProgramAddress(input.tokenProgram), [
|
|
100
|
+
meta(input.sourceTokenAccount, true),
|
|
101
|
+
meta(input.allowedMint),
|
|
102
|
+
meta(mandate),
|
|
103
|
+
meta(input.owner, false, true),
|
|
104
|
+
], encodeApproveChecked(amount, input.decimals));
|
|
105
|
+
}
|
|
106
|
+
export function buildRevokeDelegateInstruction(sourceTokenAccount, owner, tokenProgram) {
|
|
107
|
+
return instruction("revoke_delegate", tokenProgramAddress(tokenProgram), [meta(sourceTokenAccount, true), meta(owner, false, true)], encodeRevokeDelegate());
|
|
108
|
+
}
|
|
109
|
+
export function buildCreateMandateTransaction(input, owner, programId = DEFAULT_PROGRAM_ID, decimals) {
|
|
110
|
+
const mandateNonce = normalizedMandateNonce(input);
|
|
111
|
+
const normalizedInput = { ...input, mandateNonce };
|
|
112
|
+
validateMandateInput(normalizedInput);
|
|
113
|
+
const mandateAddress = deriveVersionedMandateAddress(owner, input.allowedMint, mandateNonce, programId);
|
|
114
|
+
if (decimals === undefined) {
|
|
115
|
+
throw new Error("Token decimals are required to prepare delegate approval");
|
|
116
|
+
}
|
|
117
|
+
const create = buildCreateMandateInstruction(normalizedInput, owner, programId);
|
|
118
|
+
const approve = buildApproveDelegateInstruction({
|
|
119
|
+
...normalizedInput,
|
|
120
|
+
owner,
|
|
121
|
+
mandate: mandateAddress,
|
|
122
|
+
decimals,
|
|
123
|
+
});
|
|
124
|
+
const transaction = {
|
|
125
|
+
instructions: [create, approve],
|
|
126
|
+
requiredSigners: [owner],
|
|
127
|
+
feePayer: owner,
|
|
128
|
+
};
|
|
129
|
+
return {
|
|
130
|
+
mandateAddress,
|
|
131
|
+
configAddress: deriveConfigAddress(programId),
|
|
132
|
+
transaction,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export function buildUpdateMandateInstruction(input, owner, programId = DEFAULT_PROGRAM_ID, mandateAddress) {
|
|
136
|
+
publicKey(owner);
|
|
137
|
+
publicKey(input.approvedAgent);
|
|
138
|
+
writeU64(input.maxPerPayment, "maxPerPayment");
|
|
139
|
+
writeU64(input.totalLimit, "totalLimit");
|
|
140
|
+
writeU64(input.expiresAtSlot, "expiresAtSlot");
|
|
141
|
+
writeU64(input.maxPaymentCount, "maxPaymentCount");
|
|
142
|
+
writeU64(input.cooldownSlots, "cooldownSlots");
|
|
143
|
+
if (input.maxPerPayment <= 0n)
|
|
144
|
+
throw new Error("maxPerPayment must be positive");
|
|
145
|
+
if (input.totalLimit < input.maxPerPayment)
|
|
146
|
+
throw new Error("totalLimit must cover maxPerPayment");
|
|
147
|
+
return instruction("update_mandate", programId, [
|
|
148
|
+
meta(mandateAddress ?? deriveMandateAddress(owner, programId), true),
|
|
149
|
+
meta(owner, false, true),
|
|
150
|
+
], encodeUpdateMandate(input));
|
|
151
|
+
}
|
|
152
|
+
export function buildPauseMandateInstruction(owner, programId = DEFAULT_PROGRAM_ID, mandateAddress) {
|
|
153
|
+
return instruction("pause_mandate", programId, [meta(mandateAddress ?? deriveMandateAddress(owner, programId), true), meta(owner, false, true)], encodePauseMandate());
|
|
154
|
+
}
|
|
155
|
+
export function buildRevokeMandateInstruction(owner, programId = DEFAULT_PROGRAM_ID, mandateAddress) {
|
|
156
|
+
return instruction("revoke_mandate", programId, [meta(mandateAddress ?? deriveMandateAddress(owner, programId), true), meta(owner, false, true)], encodeRevokeMandate());
|
|
157
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { ChainPayClient } from "./client.js";
|
|
2
|
+
import type { Address, Mandate, MandateStatus, PaymentReceipt, PaymentRequestPayload, PaymentStatus, ReceiptPolicy, ReceiptPolicySource } from "./types.js";
|
|
3
|
+
export declare const DEFAULT_RECEIPT_LIMIT = 10;
|
|
4
|
+
/**
|
|
5
|
+
* "Expires within about a day". Solana produces roughly 216,000 slots a day at
|
|
6
|
+
* ~400 ms each; the dashboard measures the live rate, this surface only needs
|
|
7
|
+
* a warning threshold.
|
|
8
|
+
*/
|
|
9
|
+
export declare const DEFAULT_EXPIRING_SOON_SLOTS = 216000n;
|
|
10
|
+
export type OpsAttentionKind = "expiring_soon" | "paused" | "exhausted" | "revoked" | "expired";
|
|
11
|
+
export type OpsMintTotal = {
|
|
12
|
+
mint: Address;
|
|
13
|
+
symbol: string;
|
|
14
|
+
decimals: number | null;
|
|
15
|
+
spent: string;
|
|
16
|
+
remaining: string;
|
|
17
|
+
spentBase: string;
|
|
18
|
+
remainingBase: string;
|
|
19
|
+
};
|
|
20
|
+
export type OpsMandateRow = {
|
|
21
|
+
address: Address;
|
|
22
|
+
status: MandateStatus;
|
|
23
|
+
mint: Address;
|
|
24
|
+
symbol: string;
|
|
25
|
+
decimals: number | null;
|
|
26
|
+
spent: string;
|
|
27
|
+
remaining: string;
|
|
28
|
+
totalLimit: string;
|
|
29
|
+
maxPerPayment: string;
|
|
30
|
+
spentBase: string;
|
|
31
|
+
remainingBase: string;
|
|
32
|
+
totalLimitBase: string;
|
|
33
|
+
maxPerPaymentBase: string;
|
|
34
|
+
expiresAtSlot: string;
|
|
35
|
+
approvedAgent: Address;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Spending-permission limits beside one receipt, as display amounts. `source`
|
|
39
|
+
* says where they came from; only "on-chain" was recorded on Solana. Limit
|
|
40
|
+
* fields are absent when the source is "not-recorded".
|
|
41
|
+
*/
|
|
42
|
+
export type OpsReceiptPolicy = {
|
|
43
|
+
source: ReceiptPolicySource;
|
|
44
|
+
maxPerPayment?: string;
|
|
45
|
+
totalLimit?: string;
|
|
46
|
+
/** Absent for a relay read taken after a later payment. */
|
|
47
|
+
spentAfter?: string;
|
|
48
|
+
paymentCountAfter?: string;
|
|
49
|
+
/** "0" means no payment-count cap. */
|
|
50
|
+
maxPaymentCount?: string;
|
|
51
|
+
expiresAtSlot?: string;
|
|
52
|
+
observedAtSlot?: string;
|
|
53
|
+
};
|
|
54
|
+
/** What was bought, from a merchant-signed request checked against the receipt. */
|
|
55
|
+
export type OpsReceiptPurpose = Partial<Pick<PaymentRequestPayload, "invoice" | "description" | "lineItems">> & {
|
|
56
|
+
/** False when the receipt paid a different amount, mint, or recipient than the request named. */
|
|
57
|
+
matched?: boolean;
|
|
58
|
+
};
|
|
59
|
+
export declare function opsReceiptPolicy(policy: ReceiptPolicy, decimals: number | null): OpsReceiptPolicy;
|
|
60
|
+
export type OpsReceiptRow = {
|
|
61
|
+
address: Address;
|
|
62
|
+
mandate: Address;
|
|
63
|
+
amount: string;
|
|
64
|
+
amountBase: string;
|
|
65
|
+
symbol: string;
|
|
66
|
+
decimals: number | null;
|
|
67
|
+
status: PaymentStatus;
|
|
68
|
+
executedAtSlot: string;
|
|
69
|
+
recipientTokenAccount: Address;
|
|
70
|
+
receiptUrl?: string;
|
|
71
|
+
policy: OpsReceiptPolicy;
|
|
72
|
+
purpose?: OpsReceiptPurpose;
|
|
73
|
+
};
|
|
74
|
+
export type OpsAttentionItem = {
|
|
75
|
+
kind: OpsAttentionKind;
|
|
76
|
+
mandate: Address;
|
|
77
|
+
detail: string;
|
|
78
|
+
};
|
|
79
|
+
export type OpsSnapshot = {
|
|
80
|
+
kind: "spend_overview";
|
|
81
|
+
owner: Address;
|
|
82
|
+
totals: OpsMintTotal[];
|
|
83
|
+
mandates: OpsMandateRow[];
|
|
84
|
+
receipts: OpsReceiptRow[];
|
|
85
|
+
attention: OpsAttentionItem[];
|
|
86
|
+
note: string;
|
|
87
|
+
};
|
|
88
|
+
export type OpsReceiptList = {
|
|
89
|
+
kind: "receipt_list";
|
|
90
|
+
owner: Address;
|
|
91
|
+
count: number;
|
|
92
|
+
receipts: OpsReceiptRow[];
|
|
93
|
+
};
|
|
94
|
+
export type PaymentLookupCard = {
|
|
95
|
+
kind: "payment_lookup";
|
|
96
|
+
found: boolean;
|
|
97
|
+
policy?: OpsReceiptPolicy;
|
|
98
|
+
purpose?: OpsReceiptPurpose;
|
|
99
|
+
receiptAddress?: Address;
|
|
100
|
+
amount?: string;
|
|
101
|
+
symbol?: string;
|
|
102
|
+
status?: string;
|
|
103
|
+
signature?: string;
|
|
104
|
+
mandate?: Address;
|
|
105
|
+
receiptUrl?: string;
|
|
106
|
+
};
|
|
107
|
+
export type LoadOpsSnapshotInput = {
|
|
108
|
+
owner: Address;
|
|
109
|
+
/** Keep only the mandates this caller may see. Absent means every mandate the owner has. */
|
|
110
|
+
mandateFilter?: (mandate: Mandate) => boolean;
|
|
111
|
+
receiptLimit?: number;
|
|
112
|
+
appUrl?: string;
|
|
113
|
+
currentSlot?: bigint;
|
|
114
|
+
expiringSoonSlots?: bigint;
|
|
115
|
+
/**
|
|
116
|
+
* Optional off-chain context for each listed receipt, such as a relay
|
|
117
|
+
* observation of the limits or the verified merchant request. Called only
|
|
118
|
+
* for receipts inside the limit. A failure leaves the row as it was.
|
|
119
|
+
*/
|
|
120
|
+
receiptContext?: (receipt: PaymentReceipt) => Promise<OpsReceiptContext>;
|
|
121
|
+
};
|
|
122
|
+
export type OpsReceiptContext = {
|
|
123
|
+
/** Used only when the receipt carries no on-chain snapshot. */
|
|
124
|
+
relayPolicy?: ReceiptPolicy | null;
|
|
125
|
+
purpose?: OpsReceiptPurpose;
|
|
126
|
+
};
|
|
127
|
+
export declare function tokenLabel(mint: Address): string;
|
|
128
|
+
export declare function shortAddress(value: string | undefined): string;
|
|
129
|
+
export declare function receiptUrlForAddress(receiptAddress: string | undefined, appUrl?: string): string | undefined;
|
|
130
|
+
export declare function humanTokenAmount(amount: bigint, decimals: number | null): {
|
|
131
|
+
display: string;
|
|
132
|
+
base: string;
|
|
133
|
+
decimals: number | null;
|
|
134
|
+
};
|
|
135
|
+
export declare function buildOpsSnapshot(input: {
|
|
136
|
+
owner: Address;
|
|
137
|
+
mandates: Mandate[];
|
|
138
|
+
receipts: PaymentReceipt[];
|
|
139
|
+
decimalsByMint: Record<string, number | null>;
|
|
140
|
+
currentSlot?: bigint;
|
|
141
|
+
appUrl?: string;
|
|
142
|
+
receiptLimit?: number;
|
|
143
|
+
expiringSoonSlots?: bigint;
|
|
144
|
+
}): OpsSnapshot;
|
|
145
|
+
export declare function loadOpsSnapshot(client: ChainPayClient, options: LoadOpsSnapshotInput): Promise<OpsSnapshot>;
|
|
146
|
+
export declare function receiptListFromSnapshot(snapshot: OpsSnapshot): OpsReceiptList;
|
|
147
|
+
export declare function formatOpsMarkdown(snapshot: OpsSnapshot): string;
|
|
148
|
+
export declare function formatReceiptListMarkdown(list: OpsReceiptList): string;
|
|
149
|
+
/** One plain line: the limits at payment and where they came from. */
|
|
150
|
+
export declare function receiptPolicyLine(policy: OpsReceiptPolicy | undefined, symbol: string): string;
|
|
151
|
+
export declare function formatPaymentLookupMarkdown(card: PaymentLookupCard): string;
|
|
152
|
+
export declare function formatPreparePolicyMarkdown(action: "pause" | "revoke", mandate?: string): string;
|
|
153
|
+
/** Compact terminal text. Same facts as the markdown card, no styling codes. */
|
|
154
|
+
export declare function formatOpsAnsi(snapshot: OpsSnapshot): string;
|
|
155
|
+
export declare function formatReceiptListAnsi(list: OpsReceiptList): string;
|
|
156
|
+
export declare function formatPaymentLookupAnsi(card: PaymentLookupCard): string;
|
|
157
|
+
export declare function formatPreparePolicyAnsi(action: "pause" | "revoke", mandate?: string): string;
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { assetLabel } from "./known-assets.js";
|
|
2
|
+
import { formatExactTokenAmount } from "./receipt.js";
|
|
3
|
+
import { receiptPolicy } from "./receipt-export.js";
|
|
4
|
+
export const DEFAULT_RECEIPT_LIMIT = 10;
|
|
5
|
+
/**
|
|
6
|
+
* "Expires within about a day". Solana produces roughly 216,000 slots a day at
|
|
7
|
+
* ~400 ms each; the dashboard measures the live rate, this surface only needs
|
|
8
|
+
* a warning threshold.
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_EXPIRING_SOON_SLOTS = 216000n;
|
|
11
|
+
export function opsReceiptPolicy(policy, decimals) {
|
|
12
|
+
if (policy.source === "not-recorded")
|
|
13
|
+
return { source: "not-recorded" };
|
|
14
|
+
const { limits } = policy;
|
|
15
|
+
const later = policy.source === "relay-observed" && policy.includesLaterPayments;
|
|
16
|
+
return {
|
|
17
|
+
source: policy.source,
|
|
18
|
+
maxPerPayment: humanTokenAmount(limits.maxPerPayment, decimals).display,
|
|
19
|
+
totalLimit: humanTokenAmount(limits.totalLimit, decimals).display,
|
|
20
|
+
...(later ? {} : {
|
|
21
|
+
spentAfter: humanTokenAmount(limits.amountSpentAfter, decimals).display,
|
|
22
|
+
paymentCountAfter: limits.paymentCountAfter.toString(),
|
|
23
|
+
}),
|
|
24
|
+
maxPaymentCount: limits.maxPaymentCount.toString(),
|
|
25
|
+
expiresAtSlot: limits.expiresAtSlot.toString(),
|
|
26
|
+
...(policy.source === "relay-observed" ? { observedAtSlot: policy.observedAtSlot.toString() } : {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function tokenLabel(mint) {
|
|
30
|
+
return assetLabel(mint);
|
|
31
|
+
}
|
|
32
|
+
export function shortAddress(value) {
|
|
33
|
+
if (!value || value.length < 12)
|
|
34
|
+
return value ?? "unknown";
|
|
35
|
+
return `${value.slice(0, 4)}…${value.slice(-4)}`;
|
|
36
|
+
}
|
|
37
|
+
export function receiptUrlForAddress(receiptAddress, appUrl) {
|
|
38
|
+
if (!receiptAddress)
|
|
39
|
+
return undefined;
|
|
40
|
+
const origin = appUrl?.trim().replace(/\/$/, "");
|
|
41
|
+
if (!origin)
|
|
42
|
+
return undefined;
|
|
43
|
+
return `${origin}/verify/${encodeURIComponent(receiptAddress)}`;
|
|
44
|
+
}
|
|
45
|
+
export function humanTokenAmount(amount, decimals) {
|
|
46
|
+
const exact = formatExactTokenAmount(amount, decimals);
|
|
47
|
+
let display = exact.display;
|
|
48
|
+
if (exact.displayKind === "ui-amount" && display.includes(".")) {
|
|
49
|
+
display = display.replace(/0+$/, "").replace(/\.$/, "");
|
|
50
|
+
}
|
|
51
|
+
return { display, base: exact.baseUnits, decimals: exact.decimals };
|
|
52
|
+
}
|
|
53
|
+
function remainingBase(mandate) {
|
|
54
|
+
return mandate.totalLimit > mandate.amountSpent ? mandate.totalLimit - mandate.amountSpent : 0n;
|
|
55
|
+
}
|
|
56
|
+
export function buildOpsSnapshot(input) {
|
|
57
|
+
const receiptLimit = input.receiptLimit ?? DEFAULT_RECEIPT_LIMIT;
|
|
58
|
+
const soon = input.expiringSoonSlots ?? DEFAULT_EXPIRING_SOON_SLOTS;
|
|
59
|
+
const mandates = input.mandates.map((mandate) => {
|
|
60
|
+
const decimals = input.decimalsByMint[mandate.allowedMint] ?? null;
|
|
61
|
+
const symbol = tokenLabel(mandate.allowedMint);
|
|
62
|
+
const remaining = remainingBase(mandate);
|
|
63
|
+
const spent = humanTokenAmount(mandate.amountSpent, decimals);
|
|
64
|
+
const remain = humanTokenAmount(remaining, decimals);
|
|
65
|
+
const limit = humanTokenAmount(mandate.totalLimit, decimals);
|
|
66
|
+
const cap = humanTokenAmount(mandate.maxPerPayment, decimals);
|
|
67
|
+
return {
|
|
68
|
+
address: mandate.address,
|
|
69
|
+
status: mandate.status,
|
|
70
|
+
mint: mandate.allowedMint,
|
|
71
|
+
symbol,
|
|
72
|
+
decimals,
|
|
73
|
+
spent: spent.display,
|
|
74
|
+
remaining: remain.display,
|
|
75
|
+
totalLimit: limit.display,
|
|
76
|
+
maxPerPayment: cap.display,
|
|
77
|
+
spentBase: spent.base,
|
|
78
|
+
remainingBase: remain.base,
|
|
79
|
+
totalLimitBase: limit.base,
|
|
80
|
+
maxPerPaymentBase: cap.base,
|
|
81
|
+
expiresAtSlot: mandate.expiresAtSlot.toString(),
|
|
82
|
+
approvedAgent: mandate.approvedAgent,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
const mints = [...new Set(input.mandates.map((mandate) => mandate.allowedMint))];
|
|
86
|
+
const totals = mints.map((mint) => {
|
|
87
|
+
const matching = input.mandates.filter((mandate) => mandate.allowedMint === mint);
|
|
88
|
+
const spent = matching.reduce((sum, mandate) => sum + mandate.amountSpent, 0n);
|
|
89
|
+
const remaining = matching
|
|
90
|
+
.filter((mandate) => mandate.status === "active")
|
|
91
|
+
.reduce((sum, mandate) => sum + remainingBase(mandate), 0n);
|
|
92
|
+
const decimals = input.decimalsByMint[mint] ?? null;
|
|
93
|
+
const spentAmt = humanTokenAmount(spent, decimals);
|
|
94
|
+
const remainAmt = humanTokenAmount(remaining, decimals);
|
|
95
|
+
return {
|
|
96
|
+
mint,
|
|
97
|
+
symbol: tokenLabel(mint),
|
|
98
|
+
decimals,
|
|
99
|
+
spent: spentAmt.display,
|
|
100
|
+
remaining: remainAmt.display,
|
|
101
|
+
spentBase: spentAmt.base,
|
|
102
|
+
remainingBase: remainAmt.base,
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
const receipts = [...input.receipts]
|
|
106
|
+
.sort((left, right) => (right.executedAtSlot > left.executedAtSlot ? 1 : right.executedAtSlot < left.executedAtSlot ? -1 : 0))
|
|
107
|
+
.slice(0, receiptLimit)
|
|
108
|
+
.map((receipt) => {
|
|
109
|
+
const decimals = input.decimalsByMint[receipt.mint] ?? null;
|
|
110
|
+
const amount = humanTokenAmount(receipt.amount, decimals);
|
|
111
|
+
const receiptUrl = receiptUrlForAddress(receipt.address, input.appUrl);
|
|
112
|
+
return {
|
|
113
|
+
address: receipt.address,
|
|
114
|
+
mandate: receipt.mandate,
|
|
115
|
+
amount: amount.display,
|
|
116
|
+
amountBase: amount.base,
|
|
117
|
+
symbol: tokenLabel(receipt.mint),
|
|
118
|
+
decimals,
|
|
119
|
+
status: receipt.status,
|
|
120
|
+
executedAtSlot: receipt.executedAtSlot.toString(),
|
|
121
|
+
recipientTokenAccount: receipt.recipientTokenAccount,
|
|
122
|
+
...(receiptUrl ? { receiptUrl } : {}),
|
|
123
|
+
policy: opsReceiptPolicy(receiptPolicy(receipt), decimals),
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
const attention = [];
|
|
127
|
+
for (const mandate of input.mandates) {
|
|
128
|
+
if (mandate.status === "paused") {
|
|
129
|
+
attention.push({
|
|
130
|
+
kind: "paused",
|
|
131
|
+
mandate: mandate.address,
|
|
132
|
+
detail: "Paused — the agent cannot spend until the owner resumes this permission.",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
else if (mandate.status === "revoked") {
|
|
136
|
+
attention.push({
|
|
137
|
+
kind: "revoked",
|
|
138
|
+
mandate: mandate.address,
|
|
139
|
+
detail: "Revoked — settled receipts remain; the agent cannot spend.",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
else if (mandate.status === "expired") {
|
|
143
|
+
attention.push({
|
|
144
|
+
kind: "expired",
|
|
145
|
+
mandate: mandate.address,
|
|
146
|
+
detail: "Expired — the agent cannot spend. Create a new permission in the dashboard.",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else if (mandate.status === "active" && remainingBase(mandate) === 0n && mandate.totalLimit > 0n) {
|
|
150
|
+
attention.push({
|
|
151
|
+
kind: "exhausted",
|
|
152
|
+
mandate: mandate.address,
|
|
153
|
+
detail: "Remaining allowance is 0. The agent cannot spend more on this permission.",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
else if (mandate.status === "active"
|
|
157
|
+
&& input.currentSlot !== undefined
|
|
158
|
+
&& mandate.expiresAtSlot > input.currentSlot
|
|
159
|
+
&& mandate.expiresAtSlot - input.currentSlot <= soon) {
|
|
160
|
+
attention.push({
|
|
161
|
+
kind: "expiring_soon",
|
|
162
|
+
mandate: mandate.address,
|
|
163
|
+
detail: "Expires within about a day (slot estimate).",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
kind: "spend_overview",
|
|
169
|
+
owner: input.owner,
|
|
170
|
+
totals,
|
|
171
|
+
mandates,
|
|
172
|
+
receipts,
|
|
173
|
+
attention,
|
|
174
|
+
note: "Totals are mandate allowances, not wallet balance. Inbox requests stay in the dashboard until they settle on-chain.",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
export async function loadOpsSnapshot(client, options) {
|
|
178
|
+
const owner = options.owner;
|
|
179
|
+
const loaded = await client.getMandatesByOwner(owner);
|
|
180
|
+
const mandates = options.mandateFilter ? loaded.filter(options.mandateFilter) : loaded;
|
|
181
|
+
const receipts = (await Promise.all(mandates.map((mandate) => client.getPaymentsByMandate(mandate.address)))).flat();
|
|
182
|
+
const mints = [...new Set([
|
|
183
|
+
...mandates.map((mandate) => mandate.allowedMint),
|
|
184
|
+
...receipts.map((receipt) => receipt.mint),
|
|
185
|
+
])];
|
|
186
|
+
const decimalsByMint = {};
|
|
187
|
+
await Promise.all(mints.map(async (mint) => {
|
|
188
|
+
try {
|
|
189
|
+
decimalsByMint[mint] = await client.getMintDecimals(mint);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
decimalsByMint[mint] = null;
|
|
193
|
+
}
|
|
194
|
+
}));
|
|
195
|
+
const currentSlot = options.currentSlot ?? await client.getCurrentSlot().catch(() => undefined);
|
|
196
|
+
const snapshot = buildOpsSnapshot({
|
|
197
|
+
owner,
|
|
198
|
+
mandates,
|
|
199
|
+
receipts,
|
|
200
|
+
decimalsByMint,
|
|
201
|
+
currentSlot,
|
|
202
|
+
appUrl: options.appUrl,
|
|
203
|
+
receiptLimit: options.receiptLimit,
|
|
204
|
+
expiringSoonSlots: options.expiringSoonSlots,
|
|
205
|
+
});
|
|
206
|
+
const receiptContext = options.receiptContext;
|
|
207
|
+
if (!receiptContext)
|
|
208
|
+
return snapshot;
|
|
209
|
+
const byAddress = new Map(receipts.map((receipt) => [receipt.address, receipt]));
|
|
210
|
+
const rows = await Promise.all(snapshot.receipts.map(async (row) => {
|
|
211
|
+
const receipt = byAddress.get(row.address);
|
|
212
|
+
if (!receipt)
|
|
213
|
+
return row;
|
|
214
|
+
try {
|
|
215
|
+
const context = await receiptContext(receipt);
|
|
216
|
+
return {
|
|
217
|
+
...row,
|
|
218
|
+
policy: opsReceiptPolicy(receiptPolicy(receipt, context.relayPolicy), row.decimals),
|
|
219
|
+
...(context.purpose ? { purpose: context.purpose } : {}),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return row;
|
|
224
|
+
}
|
|
225
|
+
}));
|
|
226
|
+
return { ...snapshot, receipts: rows };
|
|
227
|
+
}
|
|
228
|
+
export function receiptListFromSnapshot(snapshot) {
|
|
229
|
+
return {
|
|
230
|
+
kind: "receipt_list",
|
|
231
|
+
owner: snapshot.owner,
|
|
232
|
+
count: snapshot.receipts.length,
|
|
233
|
+
receipts: snapshot.receipts,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
export function formatOpsMarkdown(snapshot) {
|
|
237
|
+
if (snapshot.mandates.length === 0) {
|
|
238
|
+
return [
|
|
239
|
+
"**No spending permissions found** for this wallet on ChainPay Devnet.",
|
|
240
|
+
"",
|
|
241
|
+
snapshot.note,
|
|
242
|
+
"",
|
|
243
|
+
"**Next step:** Create a mandate in the ChainPay dashboard, then reconnect the agent.",
|
|
244
|
+
].join("\n");
|
|
245
|
+
}
|
|
246
|
+
const lines = ["**Spending overview**", ""];
|
|
247
|
+
if (snapshot.totals.length) {
|
|
248
|
+
lines.push("**Totals (mandate allowance, not wallet balance)**");
|
|
249
|
+
for (const total of snapshot.totals) {
|
|
250
|
+
lines.push(`- **${total.spent} ${total.symbol}** spent · **${total.remaining} ${total.symbol}** remaining`);
|
|
251
|
+
}
|
|
252
|
+
lines.push("");
|
|
253
|
+
}
|
|
254
|
+
lines.push(`**${snapshot.mandates.length} permission${snapshot.mandates.length === 1 ? "" : "s"}**`);
|
|
255
|
+
snapshot.mandates.forEach((mandate, index) => {
|
|
256
|
+
lines.push(`${index + 1}. **${mandate.symbol}** — ${mandate.status}`, ` Address: \`${shortAddress(mandate.address)}\``, ` - Per payment: **${mandate.maxPerPayment} ${mandate.symbol}**`, ` - Total allowance: **${mandate.totalLimit} ${mandate.symbol}** (**${mandate.spent} ${mandate.symbol}** spent, **${mandate.remaining} ${mandate.symbol}** remaining)`);
|
|
257
|
+
});
|
|
258
|
+
if (snapshot.attention.length) {
|
|
259
|
+
lines.push("", "**Needs attention**");
|
|
260
|
+
for (const item of snapshot.attention) {
|
|
261
|
+
lines.push(`- ${item.detail} (\`${shortAddress(item.mandate)}\`)`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (snapshot.receipts.length) {
|
|
265
|
+
lines.push("", `**Recent receipts (${snapshot.receipts.length})**`);
|
|
266
|
+
for (const receipt of snapshot.receipts) {
|
|
267
|
+
const verify = receipt.receiptUrl ? ` · [Verify](${receipt.receiptUrl})` : "";
|
|
268
|
+
lines.push(`- **${receipt.amount} ${receipt.symbol}** · \`${shortAddress(receipt.address)}\`${verify}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
lines.push("", snapshot.note, "", "**Next step:** Ask for `list_receipts`, inspect one receipt, or prepare a pause in the owner wallet.");
|
|
272
|
+
return lines.join("\n");
|
|
273
|
+
}
|
|
274
|
+
export function formatReceiptListMarkdown(list) {
|
|
275
|
+
if (list.count === 0) {
|
|
276
|
+
return [
|
|
277
|
+
"**No receipts yet** for this permission.",
|
|
278
|
+
"",
|
|
279
|
+
"**Next step:** After a payment settles, call `list_receipts` again or open the public verify link from the settlement card.",
|
|
280
|
+
].join("\n");
|
|
281
|
+
}
|
|
282
|
+
const lines = [`**${list.count} receipt${list.count === 1 ? "" : "s"}**`, ""];
|
|
283
|
+
list.receipts.forEach((receipt, index) => {
|
|
284
|
+
const verify = receipt.receiptUrl ? ` · [Verify](${receipt.receiptUrl})` : "";
|
|
285
|
+
lines.push(`${index + 1}. **${receipt.amount} ${receipt.symbol}** — ${receipt.status}`, ...(receipt.purpose?.description ? [` For: ${receipt.purpose.description}`] : []), ` Receipt: \`${shortAddress(receipt.address)}\`${verify}`, ` Permission: \`${shortAddress(receipt.mandate)}\``, ` ${receiptPolicyLine(receipt.policy, receipt.symbol)}`);
|
|
286
|
+
});
|
|
287
|
+
lines.push("", "**Next step:** Open a verify link or call `get_payment` with a receipt address for the full card.");
|
|
288
|
+
return lines.join("\n");
|
|
289
|
+
}
|
|
290
|
+
const POLICY_SOURCE_LABEL = {
|
|
291
|
+
"on-chain": "recorded on Solana at payment",
|
|
292
|
+
"relay-observed": "seen by the ChainPay relay after payment, not stored on Solana",
|
|
293
|
+
"not-recorded": "not recorded for this receipt",
|
|
294
|
+
};
|
|
295
|
+
/** One plain line: the limits at payment and where they came from. */
|
|
296
|
+
export function receiptPolicyLine(policy, symbol) {
|
|
297
|
+
if (!policy || policy.source === "not-recorded" || !policy.maxPerPayment || !policy.totalLimit) {
|
|
298
|
+
return `Limits at payment: ${POLICY_SOURCE_LABEL["not-recorded"]}`;
|
|
299
|
+
}
|
|
300
|
+
const spent = policy.spentAfter ? ` · ${policy.spentAfter} of ${policy.totalLimit} ${symbol} used after` : ` · ${policy.totalLimit} ${symbol} total`;
|
|
301
|
+
return `Limits at payment: ${policy.maxPerPayment} ${symbol} per payment${spent} (${POLICY_SOURCE_LABEL[policy.source]})`;
|
|
302
|
+
}
|
|
303
|
+
export function formatPaymentLookupMarkdown(card) {
|
|
304
|
+
if (!card.found) {
|
|
305
|
+
const address = card.receiptAddress ? ` at \`${shortAddress(card.receiptAddress)}\`` : "";
|
|
306
|
+
return `**Receipt not found**${address}.\n\n**Next step:** Confirm the address or call \`list_receipts\` for this permission.`;
|
|
307
|
+
}
|
|
308
|
+
const lines = [
|
|
309
|
+
"**Payment receipt** on Solana Devnet.",
|
|
310
|
+
...(card.amount ? [`- Amount: **${card.amount}${card.symbol ? ` ${card.symbol}` : ""}**`] : []),
|
|
311
|
+
...(card.receiptAddress ? [`- Receipt: \`${shortAddress(card.receiptAddress)}\``] : []),
|
|
312
|
+
...(card.purpose?.description ? [`- For: ${card.purpose.description}`] : []),
|
|
313
|
+
...(card.mandate ? [`- Permission: \`${shortAddress(card.mandate)}\``] : []),
|
|
314
|
+
...(card.policy ? [`- ${receiptPolicyLine(card.policy, card.symbol ?? "")}`] : []),
|
|
315
|
+
...(card.status ? [`- Status: ${card.status}`] : []),
|
|
316
|
+
...(card.signature ? [`- Signature: \`${shortAddress(card.signature)}\``] : []),
|
|
317
|
+
...(card.receiptUrl ? [`- Verify: ${card.receiptUrl}`] : []),
|
|
318
|
+
"",
|
|
319
|
+
"**Next step:** Share the verify link or open the receipt in ChainPay.",
|
|
320
|
+
];
|
|
321
|
+
return lines.join("\n");
|
|
322
|
+
}
|
|
323
|
+
export function formatPreparePolicyMarkdown(action, mandate) {
|
|
324
|
+
const verb = action === "pause" ? "pause" : "revoke";
|
|
325
|
+
return [
|
|
326
|
+
`**Owner wallet signature required** — ${verb} this permission.`,
|
|
327
|
+
"",
|
|
328
|
+
"This change needs the owner wallet—not the agent connection.",
|
|
329
|
+
...(mandate ? [`- Permission: \`${shortAddress(mandate)}\``] : []),
|
|
330
|
+
"",
|
|
331
|
+
"**Options**",
|
|
332
|
+
"1. Approve in the ChainPay dashboard wallet flow",
|
|
333
|
+
"2. Cancel and keep the current policy",
|
|
334
|
+
"",
|
|
335
|
+
"**Next step:** Wait for the owner to sign in the dashboard. This surface does not sign or submit.",
|
|
336
|
+
].join("\n");
|
|
337
|
+
}
|
|
338
|
+
function stripMarkdown(value) {
|
|
339
|
+
return value
|
|
340
|
+
.replace(/\*\*/g, "")
|
|
341
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
342
|
+
.replace(/`/g, "");
|
|
343
|
+
}
|
|
344
|
+
/** Compact terminal text. Same facts as the markdown card, no styling codes. */
|
|
345
|
+
export function formatOpsAnsi(snapshot) {
|
|
346
|
+
return stripMarkdown(formatOpsMarkdown(snapshot));
|
|
347
|
+
}
|
|
348
|
+
export function formatReceiptListAnsi(list) {
|
|
349
|
+
return stripMarkdown(formatReceiptListMarkdown(list));
|
|
350
|
+
}
|
|
351
|
+
export function formatPaymentLookupAnsi(card) {
|
|
352
|
+
return stripMarkdown(formatPaymentLookupMarkdown(card));
|
|
353
|
+
}
|
|
354
|
+
export function formatPreparePolicyAnsi(action, mandate) {
|
|
355
|
+
return stripMarkdown(formatPreparePolicyMarkdown(action, mandate));
|
|
356
|
+
}
|