@canopy-finance/x402-client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -0
- package/dist/index.cjs +187 -0
- package/dist/index.d.cts +158 -0
- package/dist/index.d.ts +158 -0
- package/dist/index.mjs +172 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# @canopy-finance/x402-client
|
|
2
|
+
|
|
3
|
+
Buyer-side [x402](https://x402.org) client for the Canopy facilitator. Build and
|
|
4
|
+
post **`exact-permit2-v2`** payments — an EIP-712 Permit2 witness that binds both
|
|
5
|
+
the recipient and a stable `serviceId` into the payer's signature — without
|
|
6
|
+
hand-rolling the typed data.
|
|
7
|
+
|
|
8
|
+
- Works in the **browser and in Node** (uses `fetch` + `globalThis.crypto` only).
|
|
9
|
+
- **Zero runtime dependencies** except **viem** (peer).
|
|
10
|
+
- Signer-agnostic: pass any `signTypedData` (viem wallet, ethers, injected wallet…).
|
|
11
|
+
- ESM + CJS + type declarations.
|
|
12
|
+
|
|
13
|
+
The EIP-712 shape here is the single source of truth shared with the facilitator's
|
|
14
|
+
verifier, so buyer and verifier can never drift.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @canopy-finance/x402-client viem
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## The four-step buyer flow
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import {
|
|
26
|
+
getSupported,
|
|
27
|
+
permit2ApprovalTx,
|
|
28
|
+
createExactPermit2V2Payment,
|
|
29
|
+
post,
|
|
30
|
+
} from "@canopy-finance/x402-client";
|
|
31
|
+
import { createWalletClient, custom } from "viem";
|
|
32
|
+
|
|
33
|
+
const url = "https://facilitator.canopyfinance.io";
|
|
34
|
+
|
|
35
|
+
// 1. Discover the facilitator's advertised kind (validates schemeVersion = 2).
|
|
36
|
+
const kind = await getSupported(url);
|
|
37
|
+
|
|
38
|
+
// 2. (once per payer) Approve Permit2 to move the asset on your behalf.
|
|
39
|
+
const wallet = createWalletClient({ account, transport: custom(window.ethereum) });
|
|
40
|
+
await wallet.writeContract(permit2ApprovalTx(kind));
|
|
41
|
+
|
|
42
|
+
// 3. Build a signed payment. `serviceId` comes from the resource's 402
|
|
43
|
+
// response (paymentRequirements.extra.serviceId).
|
|
44
|
+
const body = await createExactPermit2V2Payment({
|
|
45
|
+
chainId: 4663,
|
|
46
|
+
kind,
|
|
47
|
+
owner: account.address,
|
|
48
|
+
payTo, // the seller's recipient address
|
|
49
|
+
amount: 10_000n, // atomic units (e.g. 0.01 USDG at 6 decimals)
|
|
50
|
+
serviceId, // bytes32 from the resource's 402
|
|
51
|
+
signTypedData: (args) => wallet.signTypedData({ account, ...args }),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// 4. POST to /verify, then /settle.
|
|
55
|
+
const verified = await post(url, "verify", body);
|
|
56
|
+
if (verified.isValid !== true) throw new Error("verify failed");
|
|
57
|
+
const settled = await post(url, "settle", body /*, apiKey */);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Migration from v1 (`exact-permit2`)
|
|
61
|
+
|
|
62
|
+
The legacy `exact-permit2` scheme is read-only. To integrate:
|
|
63
|
+
|
|
64
|
+
1. Read `serviceId` from the resource's 402 (`extra.serviceId`).
|
|
65
|
+
2. Call `createExactPermit2V2Payment` (the v1 creator `createPermit2Payment` now
|
|
66
|
+
throws `UnsupportedSchemeError`).
|
|
67
|
+
3. Target a facilitator advertising `schemeVersion: 2` / `payloadVersion: 2`.
|
|
68
|
+
|
|
69
|
+
## Exports
|
|
70
|
+
|
|
71
|
+
- `getSupported(url)` — fetch + validate the `exact-permit2-v2` kind.
|
|
72
|
+
- `permit2ApprovalTx(kind, amount?)` — viem `writeContract` config for the one-time approval.
|
|
73
|
+
- `createExactPermit2V2Payment(params)` — build a signed, ready-to-POST payment body.
|
|
74
|
+
- `post(url, "verify" | "settle", body, apiKey?)` — POST helper.
|
|
75
|
+
- Constants: `PERMIT2_TYPES`, `WITNESS_TYPE_STRING`, `SUPPORTED_SCHEME`,
|
|
76
|
+
`SUPPORTED_SCHEME_VERSION`, `SUPPORTED_PAYLOAD_VERSION`.
|
|
77
|
+
- Errors: `UnsupportedSchemeError`, `UnsupportedSchemeVersionError`,
|
|
78
|
+
`UnsupportedPayloadVersionError`, `MissingServiceIdError`.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var viem = require('viem');
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
var SUPPORTED_SCHEME = "exact-permit2-v2";
|
|
7
|
+
var SUPPORTED_SCHEME_VERSION = 2;
|
|
8
|
+
var SUPPORTED_PAYLOAD_VERSION = 2;
|
|
9
|
+
var UnsupportedSchemeError = class extends Error {
|
|
10
|
+
constructor(scheme) {
|
|
11
|
+
super(`unsupported scheme "${scheme}"; expected ${SUPPORTED_SCHEME}`);
|
|
12
|
+
this.scheme = scheme;
|
|
13
|
+
this.name = "UnsupportedSchemeError";
|
|
14
|
+
}
|
|
15
|
+
scheme;
|
|
16
|
+
};
|
|
17
|
+
var UnsupportedSchemeVersionError = class extends Error {
|
|
18
|
+
constructor(version) {
|
|
19
|
+
super(`unsupported schemeVersion ${String(version)}; expected ${SUPPORTED_SCHEME_VERSION}`);
|
|
20
|
+
this.version = version;
|
|
21
|
+
this.name = "UnsupportedSchemeVersionError";
|
|
22
|
+
}
|
|
23
|
+
version;
|
|
24
|
+
};
|
|
25
|
+
var UnsupportedPayloadVersionError = class extends Error {
|
|
26
|
+
constructor(version) {
|
|
27
|
+
super(`unsupported payloadVersion ${String(version)}; expected ${SUPPORTED_PAYLOAD_VERSION}`);
|
|
28
|
+
this.version = version;
|
|
29
|
+
this.name = "UnsupportedPayloadVersionError";
|
|
30
|
+
}
|
|
31
|
+
version;
|
|
32
|
+
};
|
|
33
|
+
var MissingServiceIdError = class extends Error {
|
|
34
|
+
constructor() {
|
|
35
|
+
super("serviceId is required for exact-permit2-v2; read it from the resource's 402 extra.serviceId");
|
|
36
|
+
this.name = "MissingServiceIdError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var PERMIT2_TYPES = {
|
|
40
|
+
PermitWitnessTransferFrom: [
|
|
41
|
+
{ name: "permitted", type: "TokenPermissions" },
|
|
42
|
+
{ name: "spender", type: "address" },
|
|
43
|
+
{ name: "nonce", type: "uint256" },
|
|
44
|
+
{ name: "deadline", type: "uint256" },
|
|
45
|
+
{ name: "witness", type: "X402Witness" }
|
|
46
|
+
],
|
|
47
|
+
TokenPermissions: [
|
|
48
|
+
{ name: "token", type: "address" },
|
|
49
|
+
{ name: "amount", type: "uint256" }
|
|
50
|
+
],
|
|
51
|
+
X402Witness: [
|
|
52
|
+
{ name: "to", type: "address" },
|
|
53
|
+
{ name: "serviceId", type: "bytes32" }
|
|
54
|
+
]
|
|
55
|
+
};
|
|
56
|
+
var WITNESS_TYPE_STRING = "X402Witness witness)TokenPermissions(address token,uint256 amount)X402Witness(address to,bytes32 serviceId)";
|
|
57
|
+
var isBytes32 = (v) => typeof v === "string" && /^0x[0-9a-fA-F]{64}$/u.test(v);
|
|
58
|
+
async function getSupported(facilitatorUrl) {
|
|
59
|
+
const res = await fetch(`${facilitatorUrl.replace(/\/+$/u, "")}/supported`);
|
|
60
|
+
if (!res.ok) throw new Error(`/supported failed: ${res.status}`);
|
|
61
|
+
const body = await res.json();
|
|
62
|
+
if (body.schemeVersion !== SUPPORTED_SCHEME_VERSION) {
|
|
63
|
+
throw new UnsupportedSchemeVersionError(body.schemeVersion);
|
|
64
|
+
}
|
|
65
|
+
if (body.payloadVersion !== SUPPORTED_PAYLOAD_VERSION) {
|
|
66
|
+
throw new UnsupportedPayloadVersionError(body.payloadVersion);
|
|
67
|
+
}
|
|
68
|
+
const kind = body.kinds?.find((k) => k.scheme === SUPPORTED_SCHEME);
|
|
69
|
+
if (!kind) throw new UnsupportedSchemeError(body.kinds?.[0]?.scheme ?? "none");
|
|
70
|
+
if (!kind.extra?.spender || !kind.extra.permit2 || !kind.extra.asset) {
|
|
71
|
+
throw new Error("facilitator /supported did not advertise a usable exact-permit2-v2 kind");
|
|
72
|
+
}
|
|
73
|
+
return kind;
|
|
74
|
+
}
|
|
75
|
+
function permit2ApprovalTx(kind, amount = viem.maxUint256) {
|
|
76
|
+
return {
|
|
77
|
+
address: viem.getAddress(kind.extra.asset),
|
|
78
|
+
abi: [
|
|
79
|
+
{
|
|
80
|
+
type: "function",
|
|
81
|
+
name: "approve",
|
|
82
|
+
stateMutability: "nonpayable",
|
|
83
|
+
inputs: [
|
|
84
|
+
{ name: "spender", type: "address" },
|
|
85
|
+
{ name: "amount", type: "uint256" }
|
|
86
|
+
],
|
|
87
|
+
outputs: [{ type: "bool" }]
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
functionName: "approve",
|
|
91
|
+
args: [viem.getAddress(kind.extra.permit2), amount]
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function randomNonce() {
|
|
95
|
+
const b = new Uint8Array(31);
|
|
96
|
+
globalThis.crypto.getRandomValues(b);
|
|
97
|
+
let n = 0n;
|
|
98
|
+
for (const x of b) n = n << 8n | BigInt(x);
|
|
99
|
+
return n;
|
|
100
|
+
}
|
|
101
|
+
async function createExactPermit2V2Payment(p) {
|
|
102
|
+
if (p.kind.scheme !== SUPPORTED_SCHEME) throw new UnsupportedSchemeError(p.kind.scheme);
|
|
103
|
+
if (!isBytes32(p.serviceId)) throw new MissingServiceIdError();
|
|
104
|
+
const token = viem.getAddress(p.kind.extra.asset);
|
|
105
|
+
const spender = viem.getAddress(p.kind.extra.spender);
|
|
106
|
+
const owner = viem.getAddress(p.owner);
|
|
107
|
+
const payTo = viem.getAddress(p.payTo);
|
|
108
|
+
const nonce = p.nonce ?? randomNonce();
|
|
109
|
+
const deadline = BigInt(Math.floor(Date.now() / 1e3) + (p.deadlineSeconds ?? 600));
|
|
110
|
+
const domain = {
|
|
111
|
+
name: "Permit2",
|
|
112
|
+
chainId: p.chainId,
|
|
113
|
+
verifyingContract: viem.getAddress(p.kind.extra.permit2)
|
|
114
|
+
};
|
|
115
|
+
const signature = await p.signTypedData({
|
|
116
|
+
domain,
|
|
117
|
+
types: PERMIT2_TYPES,
|
|
118
|
+
primaryType: "PermitWitnessTransferFrom",
|
|
119
|
+
message: {
|
|
120
|
+
permitted: { token, amount: p.amount },
|
|
121
|
+
spender,
|
|
122
|
+
nonce,
|
|
123
|
+
deadline,
|
|
124
|
+
witness: { to: payTo, serviceId: p.serviceId }
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
const accepted = {
|
|
128
|
+
scheme: SUPPORTED_SCHEME,
|
|
129
|
+
schemeVersion: SUPPORTED_SCHEME_VERSION,
|
|
130
|
+
payloadVersion: SUPPORTED_PAYLOAD_VERSION,
|
|
131
|
+
network: p.kind.network,
|
|
132
|
+
amount: p.amount.toString(),
|
|
133
|
+
asset: token,
|
|
134
|
+
payTo,
|
|
135
|
+
maxTimeoutSeconds: 600,
|
|
136
|
+
extra: { serviceId: p.serviceId }
|
|
137
|
+
};
|
|
138
|
+
return {
|
|
139
|
+
x402Version: 2,
|
|
140
|
+
paymentPayload: {
|
|
141
|
+
x402Version: 2,
|
|
142
|
+
accepted,
|
|
143
|
+
payload: {
|
|
144
|
+
signature,
|
|
145
|
+
owner,
|
|
146
|
+
spender,
|
|
147
|
+
serviceId: p.serviceId,
|
|
148
|
+
permit: {
|
|
149
|
+
permitted: { token, amount: p.amount.toString() },
|
|
150
|
+
nonce: nonce.toString(),
|
|
151
|
+
deadline: deadline.toString()
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
extensions: {}
|
|
155
|
+
},
|
|
156
|
+
paymentRequirements: accepted
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function createPermit2Payment() {
|
|
160
|
+
throw new UnsupportedSchemeError("exact-permit2");
|
|
161
|
+
}
|
|
162
|
+
async function post(facilitatorUrl, path, body, apiKey) {
|
|
163
|
+
const res = await fetch(`${facilitatorUrl.replace(/\/+$/u, "")}/${path}`, {
|
|
164
|
+
method: "POST",
|
|
165
|
+
headers: {
|
|
166
|
+
"Content-Type": "application/json",
|
|
167
|
+
...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
|
|
168
|
+
},
|
|
169
|
+
body: JSON.stringify(body)
|
|
170
|
+
});
|
|
171
|
+
return await res.json();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
exports.MissingServiceIdError = MissingServiceIdError;
|
|
175
|
+
exports.PERMIT2_TYPES = PERMIT2_TYPES;
|
|
176
|
+
exports.SUPPORTED_PAYLOAD_VERSION = SUPPORTED_PAYLOAD_VERSION;
|
|
177
|
+
exports.SUPPORTED_SCHEME = SUPPORTED_SCHEME;
|
|
178
|
+
exports.SUPPORTED_SCHEME_VERSION = SUPPORTED_SCHEME_VERSION;
|
|
179
|
+
exports.UnsupportedPayloadVersionError = UnsupportedPayloadVersionError;
|
|
180
|
+
exports.UnsupportedSchemeError = UnsupportedSchemeError;
|
|
181
|
+
exports.UnsupportedSchemeVersionError = UnsupportedSchemeVersionError;
|
|
182
|
+
exports.WITNESS_TYPE_STRING = WITNESS_TYPE_STRING;
|
|
183
|
+
exports.createExactPermit2V2Payment = createExactPermit2V2Payment;
|
|
184
|
+
exports.createPermit2Payment = createPermit2Payment;
|
|
185
|
+
exports.getSupported = getSupported;
|
|
186
|
+
exports.permit2ApprovalTx = permit2ApprovalTx;
|
|
187
|
+
exports.post = post;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { Address, Hex, TypedDataDomain } from 'viem';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Canopy x402 buyer SDK — construct an exact-permit2-v2 payment for the
|
|
5
|
+
* facilitator without hand-rolling the EIP-712 witness. Signer-agnostic: pass
|
|
6
|
+
* any `signTypedData` implementation (viem, ethers, a wallet, etc.).
|
|
7
|
+
*
|
|
8
|
+
* exact-permit2-v2 binds a stable `serviceId` (from the resource's 402
|
|
9
|
+
* `paymentRequirements.extra.serviceId`) into the signed witness, so a payment
|
|
10
|
+
* attributes to exactly one Discovery listing.
|
|
11
|
+
*
|
|
12
|
+
* Flow:
|
|
13
|
+
* 1. const kind = await getSupported(url) // validates schemeVersion=2
|
|
14
|
+
* 2. (once) approve Permit2 on the asset // permit2ApprovalTx()
|
|
15
|
+
* 3. const body = await createExactPermit2V2Payment({..., serviceId})
|
|
16
|
+
* 4. POST body to `${url}/verify` then `${url}/settle`
|
|
17
|
+
*
|
|
18
|
+
* Migration from v1 (exact-permit2): the legacy scheme is read-only. You must
|
|
19
|
+
* (a) read serviceId from the resource's 402, (b) call
|
|
20
|
+
* createExactPermit2V2Payment instead of the removed v1 creator, and (c) target
|
|
21
|
+
* a facilitator advertising schemeVersion=2 / payloadVersion=2.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
declare const SUPPORTED_SCHEME: "exact-permit2-v2";
|
|
25
|
+
declare const SUPPORTED_SCHEME_VERSION: 2;
|
|
26
|
+
declare const SUPPORTED_PAYLOAD_VERSION: 2;
|
|
27
|
+
/** Typed errors so integrators can switch on failure modes. */
|
|
28
|
+
declare class UnsupportedSchemeError extends Error {
|
|
29
|
+
readonly scheme: string;
|
|
30
|
+
constructor(scheme: string);
|
|
31
|
+
}
|
|
32
|
+
declare class UnsupportedSchemeVersionError extends Error {
|
|
33
|
+
readonly version: unknown;
|
|
34
|
+
constructor(version: unknown);
|
|
35
|
+
}
|
|
36
|
+
declare class UnsupportedPayloadVersionError extends Error {
|
|
37
|
+
readonly version: unknown;
|
|
38
|
+
constructor(version: unknown);
|
|
39
|
+
}
|
|
40
|
+
declare class MissingServiceIdError extends Error {
|
|
41
|
+
constructor();
|
|
42
|
+
}
|
|
43
|
+
interface SupportedExtra {
|
|
44
|
+
permit2: Address;
|
|
45
|
+
spender: Address;
|
|
46
|
+
asset: Address;
|
|
47
|
+
witnessTypeString: string;
|
|
48
|
+
serviceIdRequired?: boolean;
|
|
49
|
+
}
|
|
50
|
+
interface SupportedKind {
|
|
51
|
+
x402Version: number;
|
|
52
|
+
scheme: string;
|
|
53
|
+
network: string;
|
|
54
|
+
extra: SupportedExtra;
|
|
55
|
+
}
|
|
56
|
+
/** EIP-712 types for the v2 witnessed Permit2 transfer (matches the facilitator). */
|
|
57
|
+
declare const PERMIT2_TYPES: {
|
|
58
|
+
readonly PermitWitnessTransferFrom: readonly [{
|
|
59
|
+
readonly name: "permitted";
|
|
60
|
+
readonly type: "TokenPermissions";
|
|
61
|
+
}, {
|
|
62
|
+
readonly name: "spender";
|
|
63
|
+
readonly type: "address";
|
|
64
|
+
}, {
|
|
65
|
+
readonly name: "nonce";
|
|
66
|
+
readonly type: "uint256";
|
|
67
|
+
}, {
|
|
68
|
+
readonly name: "deadline";
|
|
69
|
+
readonly type: "uint256";
|
|
70
|
+
}, {
|
|
71
|
+
readonly name: "witness";
|
|
72
|
+
readonly type: "X402Witness";
|
|
73
|
+
}];
|
|
74
|
+
readonly TokenPermissions: readonly [{
|
|
75
|
+
readonly name: "token";
|
|
76
|
+
readonly type: "address";
|
|
77
|
+
}, {
|
|
78
|
+
readonly name: "amount";
|
|
79
|
+
readonly type: "uint256";
|
|
80
|
+
}];
|
|
81
|
+
readonly X402Witness: readonly [{
|
|
82
|
+
readonly name: "to";
|
|
83
|
+
readonly type: "address";
|
|
84
|
+
}, {
|
|
85
|
+
readonly name: "serviceId";
|
|
86
|
+
readonly type: "bytes32";
|
|
87
|
+
}];
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The witnessTypeString Permit2 concatenates after its stub — FROZEN and
|
|
91
|
+
* byte-identical to the facilitator's verifier (src/verify/domain.ts). If the
|
|
92
|
+
* buyer and verifier ever disagree here, every signature silently fails.
|
|
93
|
+
*/
|
|
94
|
+
declare const WITNESS_TYPE_STRING: "X402Witness witness)TokenPermissions(address token,uint256 amount)X402Witness(address to,bytes32 serviceId)";
|
|
95
|
+
/** Fetch and validate the facilitator's advertised exact-permit2-v2 kind. */
|
|
96
|
+
declare function getSupported(facilitatorUrl: string): Promise<SupportedKind>;
|
|
97
|
+
/**
|
|
98
|
+
* The one-time approval the payer submits on-chain, so Permit2 can move the
|
|
99
|
+
* asset on their behalf. Returns a viem-style writeContract config.
|
|
100
|
+
*/
|
|
101
|
+
declare function permit2ApprovalTx(kind: SupportedKind, amount?: bigint): {
|
|
102
|
+
address: `0x${string}`;
|
|
103
|
+
abi: readonly [{
|
|
104
|
+
readonly type: "function";
|
|
105
|
+
readonly name: "approve";
|
|
106
|
+
readonly stateMutability: "nonpayable";
|
|
107
|
+
readonly inputs: readonly [{
|
|
108
|
+
readonly name: "spender";
|
|
109
|
+
readonly type: "address";
|
|
110
|
+
}, {
|
|
111
|
+
readonly name: "amount";
|
|
112
|
+
readonly type: "uint256";
|
|
113
|
+
}];
|
|
114
|
+
readonly outputs: readonly [{
|
|
115
|
+
readonly type: "bool";
|
|
116
|
+
}];
|
|
117
|
+
}];
|
|
118
|
+
functionName: "approve";
|
|
119
|
+
args: readonly [`0x${string}`, bigint];
|
|
120
|
+
};
|
|
121
|
+
type SignTypedData = (args: {
|
|
122
|
+
domain: TypedDataDomain;
|
|
123
|
+
types: typeof PERMIT2_TYPES;
|
|
124
|
+
primaryType: "PermitWitnessTransferFrom";
|
|
125
|
+
message: Record<string, unknown>;
|
|
126
|
+
}) => Promise<Hex>;
|
|
127
|
+
interface CreatePaymentParams {
|
|
128
|
+
chainId: number;
|
|
129
|
+
kind: SupportedKind;
|
|
130
|
+
owner: Address;
|
|
131
|
+
/** Recipient of the funds (the seller's payTo). */
|
|
132
|
+
payTo: Address;
|
|
133
|
+
/** Atomic amount (e.g. 10000 = 0.01 USDG at 6 decimals). */
|
|
134
|
+
amount: bigint;
|
|
135
|
+
/** Stable bytes32 serviceId from the resource's 402 (extra.serviceId). */
|
|
136
|
+
serviceId: Hex;
|
|
137
|
+
/** Unordered Permit2 nonce. Defaults to a random 255-bit value. */
|
|
138
|
+
nonce?: bigint;
|
|
139
|
+
/** Seconds until the permit expires. Default 600. */
|
|
140
|
+
deadlineSeconds?: number;
|
|
141
|
+
signTypedData: SignTypedData;
|
|
142
|
+
}
|
|
143
|
+
interface FacilitatorRequestBody {
|
|
144
|
+
x402Version: number;
|
|
145
|
+
paymentPayload: Record<string, unknown>;
|
|
146
|
+
paymentRequirements: Record<string, unknown>;
|
|
147
|
+
}
|
|
148
|
+
/** Build a signed exact-permit2-v2 payment body ready to POST to /verify + /settle. */
|
|
149
|
+
declare function createExactPermit2V2Payment(p: CreatePaymentParams): Promise<FacilitatorRequestBody>;
|
|
150
|
+
/**
|
|
151
|
+
* Deprecated v1 creator. The legacy exact-permit2 scheme is read-only; creating
|
|
152
|
+
* new legacy payments is disabled. Use createExactPermit2V2Payment.
|
|
153
|
+
*/
|
|
154
|
+
declare function createPermit2Payment(): never;
|
|
155
|
+
/** POST a payment body to the facilitator, optionally with an API key. */
|
|
156
|
+
declare function post(facilitatorUrl: string, path: "verify" | "settle", body: FacilitatorRequestBody, apiKey?: string): Promise<Record<string, unknown>>;
|
|
157
|
+
|
|
158
|
+
export { type CreatePaymentParams, type FacilitatorRequestBody, MissingServiceIdError, PERMIT2_TYPES, SUPPORTED_PAYLOAD_VERSION, SUPPORTED_SCHEME, SUPPORTED_SCHEME_VERSION, type SignTypedData, type SupportedExtra, type SupportedKind, UnsupportedPayloadVersionError, UnsupportedSchemeError, UnsupportedSchemeVersionError, WITNESS_TYPE_STRING, createExactPermit2V2Payment, createPermit2Payment, getSupported, permit2ApprovalTx, post };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { Address, Hex, TypedDataDomain } from 'viem';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Canopy x402 buyer SDK — construct an exact-permit2-v2 payment for the
|
|
5
|
+
* facilitator without hand-rolling the EIP-712 witness. Signer-agnostic: pass
|
|
6
|
+
* any `signTypedData` implementation (viem, ethers, a wallet, etc.).
|
|
7
|
+
*
|
|
8
|
+
* exact-permit2-v2 binds a stable `serviceId` (from the resource's 402
|
|
9
|
+
* `paymentRequirements.extra.serviceId`) into the signed witness, so a payment
|
|
10
|
+
* attributes to exactly one Discovery listing.
|
|
11
|
+
*
|
|
12
|
+
* Flow:
|
|
13
|
+
* 1. const kind = await getSupported(url) // validates schemeVersion=2
|
|
14
|
+
* 2. (once) approve Permit2 on the asset // permit2ApprovalTx()
|
|
15
|
+
* 3. const body = await createExactPermit2V2Payment({..., serviceId})
|
|
16
|
+
* 4. POST body to `${url}/verify` then `${url}/settle`
|
|
17
|
+
*
|
|
18
|
+
* Migration from v1 (exact-permit2): the legacy scheme is read-only. You must
|
|
19
|
+
* (a) read serviceId from the resource's 402, (b) call
|
|
20
|
+
* createExactPermit2V2Payment instead of the removed v1 creator, and (c) target
|
|
21
|
+
* a facilitator advertising schemeVersion=2 / payloadVersion=2.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
declare const SUPPORTED_SCHEME: "exact-permit2-v2";
|
|
25
|
+
declare const SUPPORTED_SCHEME_VERSION: 2;
|
|
26
|
+
declare const SUPPORTED_PAYLOAD_VERSION: 2;
|
|
27
|
+
/** Typed errors so integrators can switch on failure modes. */
|
|
28
|
+
declare class UnsupportedSchemeError extends Error {
|
|
29
|
+
readonly scheme: string;
|
|
30
|
+
constructor(scheme: string);
|
|
31
|
+
}
|
|
32
|
+
declare class UnsupportedSchemeVersionError extends Error {
|
|
33
|
+
readonly version: unknown;
|
|
34
|
+
constructor(version: unknown);
|
|
35
|
+
}
|
|
36
|
+
declare class UnsupportedPayloadVersionError extends Error {
|
|
37
|
+
readonly version: unknown;
|
|
38
|
+
constructor(version: unknown);
|
|
39
|
+
}
|
|
40
|
+
declare class MissingServiceIdError extends Error {
|
|
41
|
+
constructor();
|
|
42
|
+
}
|
|
43
|
+
interface SupportedExtra {
|
|
44
|
+
permit2: Address;
|
|
45
|
+
spender: Address;
|
|
46
|
+
asset: Address;
|
|
47
|
+
witnessTypeString: string;
|
|
48
|
+
serviceIdRequired?: boolean;
|
|
49
|
+
}
|
|
50
|
+
interface SupportedKind {
|
|
51
|
+
x402Version: number;
|
|
52
|
+
scheme: string;
|
|
53
|
+
network: string;
|
|
54
|
+
extra: SupportedExtra;
|
|
55
|
+
}
|
|
56
|
+
/** EIP-712 types for the v2 witnessed Permit2 transfer (matches the facilitator). */
|
|
57
|
+
declare const PERMIT2_TYPES: {
|
|
58
|
+
readonly PermitWitnessTransferFrom: readonly [{
|
|
59
|
+
readonly name: "permitted";
|
|
60
|
+
readonly type: "TokenPermissions";
|
|
61
|
+
}, {
|
|
62
|
+
readonly name: "spender";
|
|
63
|
+
readonly type: "address";
|
|
64
|
+
}, {
|
|
65
|
+
readonly name: "nonce";
|
|
66
|
+
readonly type: "uint256";
|
|
67
|
+
}, {
|
|
68
|
+
readonly name: "deadline";
|
|
69
|
+
readonly type: "uint256";
|
|
70
|
+
}, {
|
|
71
|
+
readonly name: "witness";
|
|
72
|
+
readonly type: "X402Witness";
|
|
73
|
+
}];
|
|
74
|
+
readonly TokenPermissions: readonly [{
|
|
75
|
+
readonly name: "token";
|
|
76
|
+
readonly type: "address";
|
|
77
|
+
}, {
|
|
78
|
+
readonly name: "amount";
|
|
79
|
+
readonly type: "uint256";
|
|
80
|
+
}];
|
|
81
|
+
readonly X402Witness: readonly [{
|
|
82
|
+
readonly name: "to";
|
|
83
|
+
readonly type: "address";
|
|
84
|
+
}, {
|
|
85
|
+
readonly name: "serviceId";
|
|
86
|
+
readonly type: "bytes32";
|
|
87
|
+
}];
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The witnessTypeString Permit2 concatenates after its stub — FROZEN and
|
|
91
|
+
* byte-identical to the facilitator's verifier (src/verify/domain.ts). If the
|
|
92
|
+
* buyer and verifier ever disagree here, every signature silently fails.
|
|
93
|
+
*/
|
|
94
|
+
declare const WITNESS_TYPE_STRING: "X402Witness witness)TokenPermissions(address token,uint256 amount)X402Witness(address to,bytes32 serviceId)";
|
|
95
|
+
/** Fetch and validate the facilitator's advertised exact-permit2-v2 kind. */
|
|
96
|
+
declare function getSupported(facilitatorUrl: string): Promise<SupportedKind>;
|
|
97
|
+
/**
|
|
98
|
+
* The one-time approval the payer submits on-chain, so Permit2 can move the
|
|
99
|
+
* asset on their behalf. Returns a viem-style writeContract config.
|
|
100
|
+
*/
|
|
101
|
+
declare function permit2ApprovalTx(kind: SupportedKind, amount?: bigint): {
|
|
102
|
+
address: `0x${string}`;
|
|
103
|
+
abi: readonly [{
|
|
104
|
+
readonly type: "function";
|
|
105
|
+
readonly name: "approve";
|
|
106
|
+
readonly stateMutability: "nonpayable";
|
|
107
|
+
readonly inputs: readonly [{
|
|
108
|
+
readonly name: "spender";
|
|
109
|
+
readonly type: "address";
|
|
110
|
+
}, {
|
|
111
|
+
readonly name: "amount";
|
|
112
|
+
readonly type: "uint256";
|
|
113
|
+
}];
|
|
114
|
+
readonly outputs: readonly [{
|
|
115
|
+
readonly type: "bool";
|
|
116
|
+
}];
|
|
117
|
+
}];
|
|
118
|
+
functionName: "approve";
|
|
119
|
+
args: readonly [`0x${string}`, bigint];
|
|
120
|
+
};
|
|
121
|
+
type SignTypedData = (args: {
|
|
122
|
+
domain: TypedDataDomain;
|
|
123
|
+
types: typeof PERMIT2_TYPES;
|
|
124
|
+
primaryType: "PermitWitnessTransferFrom";
|
|
125
|
+
message: Record<string, unknown>;
|
|
126
|
+
}) => Promise<Hex>;
|
|
127
|
+
interface CreatePaymentParams {
|
|
128
|
+
chainId: number;
|
|
129
|
+
kind: SupportedKind;
|
|
130
|
+
owner: Address;
|
|
131
|
+
/** Recipient of the funds (the seller's payTo). */
|
|
132
|
+
payTo: Address;
|
|
133
|
+
/** Atomic amount (e.g. 10000 = 0.01 USDG at 6 decimals). */
|
|
134
|
+
amount: bigint;
|
|
135
|
+
/** Stable bytes32 serviceId from the resource's 402 (extra.serviceId). */
|
|
136
|
+
serviceId: Hex;
|
|
137
|
+
/** Unordered Permit2 nonce. Defaults to a random 255-bit value. */
|
|
138
|
+
nonce?: bigint;
|
|
139
|
+
/** Seconds until the permit expires. Default 600. */
|
|
140
|
+
deadlineSeconds?: number;
|
|
141
|
+
signTypedData: SignTypedData;
|
|
142
|
+
}
|
|
143
|
+
interface FacilitatorRequestBody {
|
|
144
|
+
x402Version: number;
|
|
145
|
+
paymentPayload: Record<string, unknown>;
|
|
146
|
+
paymentRequirements: Record<string, unknown>;
|
|
147
|
+
}
|
|
148
|
+
/** Build a signed exact-permit2-v2 payment body ready to POST to /verify + /settle. */
|
|
149
|
+
declare function createExactPermit2V2Payment(p: CreatePaymentParams): Promise<FacilitatorRequestBody>;
|
|
150
|
+
/**
|
|
151
|
+
* Deprecated v1 creator. The legacy exact-permit2 scheme is read-only; creating
|
|
152
|
+
* new legacy payments is disabled. Use createExactPermit2V2Payment.
|
|
153
|
+
*/
|
|
154
|
+
declare function createPermit2Payment(): never;
|
|
155
|
+
/** POST a payment body to the facilitator, optionally with an API key. */
|
|
156
|
+
declare function post(facilitatorUrl: string, path: "verify" | "settle", body: FacilitatorRequestBody, apiKey?: string): Promise<Record<string, unknown>>;
|
|
157
|
+
|
|
158
|
+
export { type CreatePaymentParams, type FacilitatorRequestBody, MissingServiceIdError, PERMIT2_TYPES, SUPPORTED_PAYLOAD_VERSION, SUPPORTED_SCHEME, SUPPORTED_SCHEME_VERSION, type SignTypedData, type SupportedExtra, type SupportedKind, UnsupportedPayloadVersionError, UnsupportedSchemeError, UnsupportedSchemeVersionError, WITNESS_TYPE_STRING, createExactPermit2V2Payment, createPermit2Payment, getSupported, permit2ApprovalTx, post };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { maxUint256, getAddress } from 'viem';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var SUPPORTED_SCHEME = "exact-permit2-v2";
|
|
5
|
+
var SUPPORTED_SCHEME_VERSION = 2;
|
|
6
|
+
var SUPPORTED_PAYLOAD_VERSION = 2;
|
|
7
|
+
var UnsupportedSchemeError = class extends Error {
|
|
8
|
+
constructor(scheme) {
|
|
9
|
+
super(`unsupported scheme "${scheme}"; expected ${SUPPORTED_SCHEME}`);
|
|
10
|
+
this.scheme = scheme;
|
|
11
|
+
this.name = "UnsupportedSchemeError";
|
|
12
|
+
}
|
|
13
|
+
scheme;
|
|
14
|
+
};
|
|
15
|
+
var UnsupportedSchemeVersionError = class extends Error {
|
|
16
|
+
constructor(version) {
|
|
17
|
+
super(`unsupported schemeVersion ${String(version)}; expected ${SUPPORTED_SCHEME_VERSION}`);
|
|
18
|
+
this.version = version;
|
|
19
|
+
this.name = "UnsupportedSchemeVersionError";
|
|
20
|
+
}
|
|
21
|
+
version;
|
|
22
|
+
};
|
|
23
|
+
var UnsupportedPayloadVersionError = class extends Error {
|
|
24
|
+
constructor(version) {
|
|
25
|
+
super(`unsupported payloadVersion ${String(version)}; expected ${SUPPORTED_PAYLOAD_VERSION}`);
|
|
26
|
+
this.version = version;
|
|
27
|
+
this.name = "UnsupportedPayloadVersionError";
|
|
28
|
+
}
|
|
29
|
+
version;
|
|
30
|
+
};
|
|
31
|
+
var MissingServiceIdError = class extends Error {
|
|
32
|
+
constructor() {
|
|
33
|
+
super("serviceId is required for exact-permit2-v2; read it from the resource's 402 extra.serviceId");
|
|
34
|
+
this.name = "MissingServiceIdError";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var PERMIT2_TYPES = {
|
|
38
|
+
PermitWitnessTransferFrom: [
|
|
39
|
+
{ name: "permitted", type: "TokenPermissions" },
|
|
40
|
+
{ name: "spender", type: "address" },
|
|
41
|
+
{ name: "nonce", type: "uint256" },
|
|
42
|
+
{ name: "deadline", type: "uint256" },
|
|
43
|
+
{ name: "witness", type: "X402Witness" }
|
|
44
|
+
],
|
|
45
|
+
TokenPermissions: [
|
|
46
|
+
{ name: "token", type: "address" },
|
|
47
|
+
{ name: "amount", type: "uint256" }
|
|
48
|
+
],
|
|
49
|
+
X402Witness: [
|
|
50
|
+
{ name: "to", type: "address" },
|
|
51
|
+
{ name: "serviceId", type: "bytes32" }
|
|
52
|
+
]
|
|
53
|
+
};
|
|
54
|
+
var WITNESS_TYPE_STRING = "X402Witness witness)TokenPermissions(address token,uint256 amount)X402Witness(address to,bytes32 serviceId)";
|
|
55
|
+
var isBytes32 = (v) => typeof v === "string" && /^0x[0-9a-fA-F]{64}$/u.test(v);
|
|
56
|
+
async function getSupported(facilitatorUrl) {
|
|
57
|
+
const res = await fetch(`${facilitatorUrl.replace(/\/+$/u, "")}/supported`);
|
|
58
|
+
if (!res.ok) throw new Error(`/supported failed: ${res.status}`);
|
|
59
|
+
const body = await res.json();
|
|
60
|
+
if (body.schemeVersion !== SUPPORTED_SCHEME_VERSION) {
|
|
61
|
+
throw new UnsupportedSchemeVersionError(body.schemeVersion);
|
|
62
|
+
}
|
|
63
|
+
if (body.payloadVersion !== SUPPORTED_PAYLOAD_VERSION) {
|
|
64
|
+
throw new UnsupportedPayloadVersionError(body.payloadVersion);
|
|
65
|
+
}
|
|
66
|
+
const kind = body.kinds?.find((k) => k.scheme === SUPPORTED_SCHEME);
|
|
67
|
+
if (!kind) throw new UnsupportedSchemeError(body.kinds?.[0]?.scheme ?? "none");
|
|
68
|
+
if (!kind.extra?.spender || !kind.extra.permit2 || !kind.extra.asset) {
|
|
69
|
+
throw new Error("facilitator /supported did not advertise a usable exact-permit2-v2 kind");
|
|
70
|
+
}
|
|
71
|
+
return kind;
|
|
72
|
+
}
|
|
73
|
+
function permit2ApprovalTx(kind, amount = maxUint256) {
|
|
74
|
+
return {
|
|
75
|
+
address: getAddress(kind.extra.asset),
|
|
76
|
+
abi: [
|
|
77
|
+
{
|
|
78
|
+
type: "function",
|
|
79
|
+
name: "approve",
|
|
80
|
+
stateMutability: "nonpayable",
|
|
81
|
+
inputs: [
|
|
82
|
+
{ name: "spender", type: "address" },
|
|
83
|
+
{ name: "amount", type: "uint256" }
|
|
84
|
+
],
|
|
85
|
+
outputs: [{ type: "bool" }]
|
|
86
|
+
}
|
|
87
|
+
],
|
|
88
|
+
functionName: "approve",
|
|
89
|
+
args: [getAddress(kind.extra.permit2), amount]
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function randomNonce() {
|
|
93
|
+
const b = new Uint8Array(31);
|
|
94
|
+
globalThis.crypto.getRandomValues(b);
|
|
95
|
+
let n = 0n;
|
|
96
|
+
for (const x of b) n = n << 8n | BigInt(x);
|
|
97
|
+
return n;
|
|
98
|
+
}
|
|
99
|
+
async function createExactPermit2V2Payment(p) {
|
|
100
|
+
if (p.kind.scheme !== SUPPORTED_SCHEME) throw new UnsupportedSchemeError(p.kind.scheme);
|
|
101
|
+
if (!isBytes32(p.serviceId)) throw new MissingServiceIdError();
|
|
102
|
+
const token = getAddress(p.kind.extra.asset);
|
|
103
|
+
const spender = getAddress(p.kind.extra.spender);
|
|
104
|
+
const owner = getAddress(p.owner);
|
|
105
|
+
const payTo = getAddress(p.payTo);
|
|
106
|
+
const nonce = p.nonce ?? randomNonce();
|
|
107
|
+
const deadline = BigInt(Math.floor(Date.now() / 1e3) + (p.deadlineSeconds ?? 600));
|
|
108
|
+
const domain = {
|
|
109
|
+
name: "Permit2",
|
|
110
|
+
chainId: p.chainId,
|
|
111
|
+
verifyingContract: getAddress(p.kind.extra.permit2)
|
|
112
|
+
};
|
|
113
|
+
const signature = await p.signTypedData({
|
|
114
|
+
domain,
|
|
115
|
+
types: PERMIT2_TYPES,
|
|
116
|
+
primaryType: "PermitWitnessTransferFrom",
|
|
117
|
+
message: {
|
|
118
|
+
permitted: { token, amount: p.amount },
|
|
119
|
+
spender,
|
|
120
|
+
nonce,
|
|
121
|
+
deadline,
|
|
122
|
+
witness: { to: payTo, serviceId: p.serviceId }
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
const accepted = {
|
|
126
|
+
scheme: SUPPORTED_SCHEME,
|
|
127
|
+
schemeVersion: SUPPORTED_SCHEME_VERSION,
|
|
128
|
+
payloadVersion: SUPPORTED_PAYLOAD_VERSION,
|
|
129
|
+
network: p.kind.network,
|
|
130
|
+
amount: p.amount.toString(),
|
|
131
|
+
asset: token,
|
|
132
|
+
payTo,
|
|
133
|
+
maxTimeoutSeconds: 600,
|
|
134
|
+
extra: { serviceId: p.serviceId }
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
x402Version: 2,
|
|
138
|
+
paymentPayload: {
|
|
139
|
+
x402Version: 2,
|
|
140
|
+
accepted,
|
|
141
|
+
payload: {
|
|
142
|
+
signature,
|
|
143
|
+
owner,
|
|
144
|
+
spender,
|
|
145
|
+
serviceId: p.serviceId,
|
|
146
|
+
permit: {
|
|
147
|
+
permitted: { token, amount: p.amount.toString() },
|
|
148
|
+
nonce: nonce.toString(),
|
|
149
|
+
deadline: deadline.toString()
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
extensions: {}
|
|
153
|
+
},
|
|
154
|
+
paymentRequirements: accepted
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function createPermit2Payment() {
|
|
158
|
+
throw new UnsupportedSchemeError("exact-permit2");
|
|
159
|
+
}
|
|
160
|
+
async function post(facilitatorUrl, path, body, apiKey) {
|
|
161
|
+
const res = await fetch(`${facilitatorUrl.replace(/\/+$/u, "")}/${path}`, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: {
|
|
164
|
+
"Content-Type": "application/json",
|
|
165
|
+
...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
|
|
166
|
+
},
|
|
167
|
+
body: JSON.stringify(body)
|
|
168
|
+
});
|
|
169
|
+
return await res.json();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export { MissingServiceIdError, PERMIT2_TYPES, SUPPORTED_PAYLOAD_VERSION, SUPPORTED_SCHEME, SUPPORTED_SCHEME_VERSION, UnsupportedPayloadVersionError, UnsupportedSchemeError, UnsupportedSchemeVersionError, WITNESS_TYPE_STRING, createExactPermit2V2Payment, createPermit2Payment, getSupported, permit2ApprovalTx, post };
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@canopy-finance/x402-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Buyer-side x402 client for the Canopy facilitator: build and post exact-permit2-v2 payments (EIP-712 Permit2 witness). Works in the browser and in Node. Zero runtime deps except viem (peer).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.mjs",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"main": "./dist/index.cjs",
|
|
18
|
+
"module": "./dist/index.mjs",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"viem": "^2"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"tsup": "^8.3.5",
|
|
34
|
+
"typescript": "^5.5.0",
|
|
35
|
+
"viem": "^2.21.0"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"x402",
|
|
39
|
+
"permit2",
|
|
40
|
+
"eip-712",
|
|
41
|
+
"canopy",
|
|
42
|
+
"robinhood-chain",
|
|
43
|
+
"payments",
|
|
44
|
+
"stablecoin"
|
|
45
|
+
],
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/canopyfinance/canopy-facilitator.git",
|
|
52
|
+
"directory": "packages/x402-client"
|
|
53
|
+
}
|
|
54
|
+
}
|