joinhive 2.0.1 → 2.2.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.
@@ -0,0 +1,28 @@
1
+ // shared/x402-client — the paying side of x402, in one function.
2
+ //
3
+ // GET (or POST) a priced resource; on 402, read what the server accepts, sign an
4
+ // EIP-3009 authorization for the cheapest option this payer will accept, and
5
+ // retry with the PAYMENT-SIGNATURE header. Returns the server's response.
6
+ import { parsePaymentRequired, signExactPayment, HEADERS } from './x402.mjs';
7
+
8
+ // fetchFn(url, {method, headers, body}) -> a Response-like { status, headers:{get(name)}, ... }.
9
+ // signer: an ethers Wallet (the payer). Options:
10
+ // maxValue refuse to pay more than this (base units, string/bigint)
11
+ // chooseFrom (accepts[]) -> the entry to pay (default: first `exact`)
12
+ // validForSecs authorization lifetime
13
+ export const payAndFetch = async (fetchFn, url, signer, { method = 'GET', body, headers = {}, maxValue, chooseFrom, validForSecs = 600 } = {}) => {
14
+ const first = await fetchFn(url, { method, headers, body });
15
+ if (first.status !== 402) return first; // free, or a non-payment error — pass through
16
+
17
+ const reqHeader = first.headers.get(HEADERS.REQUIRED);
18
+ if (!reqHeader) throw new Error('402 without a PAYMENT-REQUIRED header');
19
+ const { accepts } = parsePaymentRequired(reqHeader);
20
+ if (!Array.isArray(accepts) || !accepts.length) throw new Error('402 offered no payment options');
21
+
22
+ const pick = (chooseFrom ? chooseFrom(accepts) : accepts.find((a) => a.scheme === 'exact')) || accepts[0];
23
+ if (!pick) throw new Error('no acceptable payment scheme (need exact)');
24
+ if (maxValue != null && BigInt(pick.amount) > BigInt(maxValue)) throw new Error(`price ${pick.amount} exceeds maxValue ${maxValue}`);
25
+
26
+ const sig = await signExactPayment(signer, pick, { validForSecs });
27
+ return fetchFn(url, { method, headers: { ...headers, [HEADERS.SIGNATURE]: sig }, body });
28
+ };
@@ -0,0 +1,72 @@
1
+ // shared/x402 — the x402 payment protocol, `exact` scheme, over EIP-3009.
2
+ //
3
+ // x402 turns HTTP 402 into a working payment: a server answers a paid request
4
+ // with `402` + a PAYMENT-REQUIRED header describing what it accepts; the client
5
+ // signs a stablecoin authorization (EIP-3009 transferWithAuthorization — gasless,
6
+ // no prior approval) and retries with a PAYMENT-SIGNATURE header; the server (or
7
+ // a facilitator) verifies and settles it on-chain. Hive uses this for A2A: a bee
8
+ // pays another bee in JELLY (EIP-3009 via JellyV3) or test-USDC to invoke work.
9
+ //
10
+ // This module is the shared codec + the sign/verify half (pure, testable).
11
+ // On-chain settle lives in server/x402-facilitator.mjs.
12
+ import { randomBytes } from 'node:crypto';
13
+ import { verifyTypedData, getAddress } from 'ethers';
14
+
15
+ const b64 = (obj) => Buffer.from(JSON.stringify(obj)).toString('base64');
16
+ const unb64 = (s) => JSON.parse(Buffer.from(String(s), 'base64').toString('utf8'));
17
+
18
+ export const HEADERS = { REQUIRED: 'payment-required', SIGNATURE: 'payment-signature', RESPONSE: 'payment-response' };
19
+
20
+ // A fresh 32-byte authorization nonce (any unique bytes32; not sequential).
21
+ export const randomNonce = () => '0x' + randomBytes(32).toString('hex');
22
+
23
+ // EIP-712 typed-data pieces for the `exact` scheme (EIP-3009 TransferWithAuthorization).
24
+ export const exactTypes = { TransferWithAuthorization: [
25
+ { name: 'from', type: 'address' }, { name: 'to', type: 'address' }, { name: 'value', type: 'uint256' },
26
+ { name: 'validAfter', type: 'uint256' }, { name: 'validBefore', type: 'uint256' }, { name: 'nonce', type: 'bytes32' },
27
+ ] };
28
+ // domain must match the token's EIP-712 domain (JellyV3 = {name:'Jelly',version:'1'}).
29
+ export const exactDomain = ({ asset, chainId, name = 'Jelly', version = '1' }) => ({ name, version, chainId, verifyingContract: getAddress(asset) });
30
+
31
+ // ---- server: what to accept -------------------------------------------------------
32
+ // accepts[] entries: {scheme:'exact', network, asset, amount, payTo, name?, version?, maxTimeoutSecs?, description?, resource?}
33
+ export const buildPaymentRequired = ({ accepts, error = '' }) => b64({ x402Version: 1, accepts, error });
34
+ export const parsePaymentRequired = (header) => unb64(header);
35
+
36
+ // ---- client: sign an authorization -> a payment payload ---------------------------
37
+ // signer: an ethers Wallet (or anything with signTypedData). Returns the base64
38
+ // PAYMENT-SIGNATURE header value.
39
+ export const signExactPayment = async (signer, req, { validForSecs = 600, nowSec = Math.floor(Date.now() / 1000) } = {}) => {
40
+ const from = getAddress(await signer.getAddress());
41
+ const auth = {
42
+ from, to: getAddress(req.payTo), value: String(req.amount),
43
+ validAfter: String(nowSec - 5), validBefore: String(nowSec + validForSecs), nonce: req.nonce || randomNonce(),
44
+ };
45
+ const domain = exactDomain(req);
46
+ const signature = await signer.signTypedData(domain, exactTypes, auth);
47
+ return b64({ x402Version: 1, scheme: 'exact', network: req.network, asset: req.asset, authorization: auth, signature });
48
+ };
49
+ export const parsePaymentPayload = (header) => unb64(header);
50
+
51
+ // ---- verify (facilitator/server, off-chain part) ----------------------------------
52
+ // Confirms the signature recovers to `authorization.from`, and that the amount,
53
+ // recipient, and validity window satisfy the requirement. Nonce-unused + payer
54
+ // balance are confirmed ON-CHAIN at settle (this can't see them). -> {valid, from, reason}
55
+ export const verifyExact = (payload, req, { nowSec = Math.floor(Date.now() / 1000) } = {}) => {
56
+ try {
57
+ if (!payload || payload.scheme !== 'exact') return { valid: false, reason: 'scheme-mismatch' };
58
+ const a = payload.authorization || {};
59
+ if (getAddress(a.to) !== getAddress(req.payTo)) return { valid: false, reason: 'wrong-recipient' };
60
+ if (BigInt(a.value) < BigInt(req.amount)) return { valid: false, reason: 'underpaid' };
61
+ if (getAddress(payload.asset) !== getAddress(req.asset)) return { valid: false, reason: 'wrong-asset' };
62
+ if (nowSec <= Number(a.validAfter)) return { valid: false, reason: 'not-yet-valid' };
63
+ if (nowSec >= Number(a.validBefore)) return { valid: false, reason: 'expired' };
64
+ const recovered = verifyTypedData(exactDomain(req), exactTypes, a, payload.signature);
65
+ if (getAddress(recovered) !== getAddress(a.from)) return { valid: false, reason: 'bad-signature' };
66
+ return { valid: true, from: getAddress(a.from) };
67
+ } catch (e) { return { valid: false, reason: `malformed: ${String(e.message).slice(0, 60)}` }; }
68
+ };
69
+
70
+ // ---- response ---------------------------------------------------------------------
71
+ export const buildPaymentResponse = ({ success, txHash, network, payer }) => b64({ success, txHash, network, payer });
72
+ export const parsePaymentResponse = (header) => unb64(header);