@haven_ai/sdk 0.1.26-alpha.0 → 0.1.28-alpha.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/dist/index.cjs +1861 -1690
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -161
- package/dist/index.d.ts +53 -161
- package/dist/index.js +1861 -1690
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
-
import { exact } from 'x402/schemes';
|
|
3
1
|
import { recoverTypedDataAddress, hashTypedData } from 'viem';
|
|
4
|
-
import { privateKeyToAccount } from 'viem/accounts';
|
|
5
2
|
import { ethers } from 'ethers';
|
|
6
3
|
import { createHash } from 'crypto';
|
|
4
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
5
|
+
import { exact } from 'x402/schemes';
|
|
6
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
7
7
|
|
|
8
8
|
// src/client.ts
|
|
9
9
|
|
|
@@ -392,24 +392,6 @@ function verifySignature(hash, signature, expectedAddress) {
|
|
|
392
392
|
return false;
|
|
393
393
|
}
|
|
394
394
|
}
|
|
395
|
-
var RECEIPT_VERSION = "haven-receipt-1";
|
|
396
|
-
function defaultRecover(hash, signature) {
|
|
397
|
-
return ethers.recoverAddress(hash, signature);
|
|
398
|
-
}
|
|
399
|
-
function verifyPaymentReceipt(receipt, recover = defaultRecover) {
|
|
400
|
-
const { delegate, signHash: signHash2, signature } = receipt.authorization;
|
|
401
|
-
if (!signature) return { verified: false, reason: "missing_signature" };
|
|
402
|
-
let recovered;
|
|
403
|
-
try {
|
|
404
|
-
recovered = recover(signHash2, signature);
|
|
405
|
-
} catch {
|
|
406
|
-
return { verified: false, reason: "bad_signature" };
|
|
407
|
-
}
|
|
408
|
-
if (recovered.toLowerCase() !== delegate.toLowerCase()) {
|
|
409
|
-
return { verified: false, reason: "signer_mismatch", recoveredSigner: recovered };
|
|
410
|
-
}
|
|
411
|
-
return { verified: true, recoveredSigner: recovered };
|
|
412
|
-
}
|
|
413
395
|
|
|
414
396
|
// src/base64.ts
|
|
415
397
|
function normalizeBase64(value) {
|
|
@@ -956,61 +938,173 @@ function stableStringify2(value) {
|
|
|
956
938
|
const object = value;
|
|
957
939
|
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify2(object[key])}`).join(",")}}`;
|
|
958
940
|
}
|
|
959
|
-
function createJsonRpcProvider(url) {
|
|
960
|
-
return new ethers.JsonRpcProvider(url);
|
|
961
|
-
}
|
|
962
|
-
function createWallet(privateKey, provider) {
|
|
963
|
-
return new ethers.Wallet(privateKey, provider);
|
|
964
|
-
}
|
|
965
|
-
function createErc20Contract(address, abi, runner) {
|
|
966
|
-
return new ethers.Contract(address, abi, runner);
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
// src/client.ts
|
|
970
941
|
var DEFAULT_BASE_URL = "http://localhost:3001";
|
|
971
|
-
var
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
942
|
+
var DEFAULT_REQUEST_TIMEOUT = 3e4;
|
|
943
|
+
var HavenApiTransport = class {
|
|
944
|
+
apiKey;
|
|
945
|
+
baseUrl;
|
|
946
|
+
requestTimeout;
|
|
947
|
+
defaultHeaders;
|
|
948
|
+
requestContext = new AsyncLocalStorage();
|
|
949
|
+
constructor(config) {
|
|
950
|
+
this.apiKey = config.apiKey;
|
|
951
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
952
|
+
this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
|
|
953
|
+
this.defaultHeaders = { ...config.defaultHeaders ?? {} };
|
|
954
|
+
}
|
|
955
|
+
/** Run `fn` with extra headers scoped to its asynchronous Haven API work. */
|
|
956
|
+
withRequestContext(headers, fn) {
|
|
957
|
+
return this.requestContext.run({ headers: { ...headers } }, fn);
|
|
958
|
+
}
|
|
959
|
+
async post(path, body) {
|
|
960
|
+
return this.request("POST", path, body);
|
|
961
|
+
}
|
|
962
|
+
async get(path) {
|
|
963
|
+
return this.request("GET", path);
|
|
964
|
+
}
|
|
965
|
+
async request(method, path, body) {
|
|
966
|
+
const url = `${this.baseUrl}${path}`;
|
|
967
|
+
const controller = new AbortController();
|
|
968
|
+
const timeout = setTimeout(() => controller.abort(), this.requestTimeout);
|
|
969
|
+
try {
|
|
970
|
+
const contextHeaders = this.requestContext.getStore()?.headers ?? {};
|
|
971
|
+
const res = await fetch(url, {
|
|
972
|
+
method,
|
|
973
|
+
headers: {
|
|
974
|
+
"Content-Type": "application/json",
|
|
975
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
976
|
+
...this.defaultHeaders,
|
|
977
|
+
...contextHeaders
|
|
978
|
+
},
|
|
979
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
980
|
+
signal: controller.signal
|
|
981
|
+
});
|
|
982
|
+
const data = await res.json();
|
|
983
|
+
if (!res.ok) {
|
|
984
|
+
const record = data;
|
|
985
|
+
const errorText = typeof record.error === "string" ? record.error : void 0;
|
|
986
|
+
const rawDetails = record.details ?? record.detail;
|
|
987
|
+
const detailsText = typeof rawDetails === "string" ? rawDetails : rawDetails != null ? JSON.stringify(rawDetails) : void 0;
|
|
988
|
+
const message = errorText && detailsText ? `${errorText}: ${detailsText}` : errorText ?? detailsText ?? "API request failed";
|
|
989
|
+
throw new HavenApiError(message, res.status, data);
|
|
990
|
+
}
|
|
991
|
+
return data;
|
|
992
|
+
} catch (err) {
|
|
993
|
+
if (err instanceof HavenApiError) throw err;
|
|
994
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
995
|
+
throw new HavenApiError(`Request to ${path} timed out`, 408);
|
|
996
|
+
}
|
|
997
|
+
throw new HavenApiError(
|
|
998
|
+
`Request to ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
999
|
+
0
|
|
1000
|
+
);
|
|
1001
|
+
} finally {
|
|
1002
|
+
clearTimeout(timeout);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
978
1005
|
};
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1006
|
+
|
|
1007
|
+
// src/payment-mappers.ts
|
|
1008
|
+
function mapPaymentResult(raw, buildExplorerUrl2) {
|
|
1009
|
+
return {
|
|
1010
|
+
paymentId: raw.payment_id,
|
|
1011
|
+
status: raw.status,
|
|
1012
|
+
token: raw.token,
|
|
1013
|
+
amount: raw.amount,
|
|
1014
|
+
to: raw.to,
|
|
1015
|
+
txHash: raw.tx_hash,
|
|
1016
|
+
errorMessage: raw.error_message,
|
|
1017
|
+
explorerUrl: raw.explorer_url ?? (raw.tx_hash ? buildExplorerUrl2(raw.chain_id, raw.tx_hash) : null),
|
|
1018
|
+
fee: raw.fee ? {
|
|
1019
|
+
amount: raw.fee.amount,
|
|
1020
|
+
token: raw.fee.token,
|
|
1021
|
+
basisPoints: raw.fee.basis_points,
|
|
1022
|
+
applied: raw.fee.applied
|
|
1023
|
+
} : null,
|
|
1024
|
+
createdAt: raw.created_at,
|
|
1025
|
+
signedAt: raw.signed_at,
|
|
1026
|
+
submittedAt: raw.submitted_at,
|
|
1027
|
+
confirmedAt: raw.confirmed_at,
|
|
1028
|
+
expiresAt: raw.expires_at
|
|
1029
|
+
};
|
|
985
1030
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1031
|
+
function mapPaymentStatusResult(raw) {
|
|
1032
|
+
return {
|
|
1033
|
+
paymentId: raw.payment_id,
|
|
1034
|
+
kind: raw.kind,
|
|
1035
|
+
rail: raw.rail,
|
|
1036
|
+
status: raw.status,
|
|
1037
|
+
phase: raw.phase,
|
|
1038
|
+
nextAction: raw.next_action,
|
|
1039
|
+
amount: raw.amount,
|
|
1040
|
+
token: raw.token,
|
|
1041
|
+
resourceUrl: raw.resource_url,
|
|
1042
|
+
merchantAddress: raw.merchant_address,
|
|
1043
|
+
payerAddress: raw.payer_address ?? null,
|
|
1044
|
+
txHash: raw.tx_hash,
|
|
1045
|
+
expiresAt: raw.expires_at,
|
|
1046
|
+
chainId: raw.chain_id,
|
|
1047
|
+
message: raw.message,
|
|
1048
|
+
fee: raw.fee ? {
|
|
1049
|
+
amount: raw.fee.amount,
|
|
1050
|
+
token: raw.fee.token,
|
|
1051
|
+
basisPoints: raw.fee.basis_points,
|
|
1052
|
+
applied: raw.fee.applied
|
|
1053
|
+
} : null,
|
|
1054
|
+
amountAtomic: raw.amount_atomic ?? raw.x402?.amount_atomic ?? null,
|
|
1055
|
+
asset: raw.asset ?? raw.x402?.asset ?? null,
|
|
1056
|
+
network: raw.network ?? raw.x402?.network ?? null,
|
|
1057
|
+
description: raw.description ?? raw.x402?.description ?? null,
|
|
1058
|
+
idempotencyKey: raw.idempotency_key ?? raw.x402?.idempotency_key ?? null,
|
|
1059
|
+
x402: raw.x402 ? {
|
|
1060
|
+
amountAtomic: raw.x402.amount_atomic ?? raw.amount_atomic ?? null,
|
|
1061
|
+
asset: raw.x402.asset ?? raw.asset ?? null,
|
|
1062
|
+
network: raw.x402.network ?? raw.network ?? null,
|
|
1063
|
+
resourceUrl: raw.x402.resource_url ?? raw.resource_url,
|
|
1064
|
+
merchantAddress: raw.x402.merchant_address ?? raw.merchant_address,
|
|
1065
|
+
description: raw.x402.description ?? raw.description ?? null,
|
|
1066
|
+
idempotencyKey: raw.x402.idempotency_key ?? raw.idempotency_key ?? null
|
|
1067
|
+
} : void 0
|
|
1068
|
+
};
|
|
997
1069
|
}
|
|
998
|
-
function
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1070
|
+
function mapPaymentReceipt(raw) {
|
|
1071
|
+
const receipt = {
|
|
1072
|
+
id: raw.id,
|
|
1073
|
+
paymentId: raw.payment_id,
|
|
1074
|
+
rail: raw.rail,
|
|
1075
|
+
proofStatus: raw.proof_status,
|
|
1076
|
+
txHash: raw.tx_hash,
|
|
1077
|
+
chainId: raw.chain_id,
|
|
1078
|
+
resourceUrl: raw.resource_url,
|
|
1079
|
+
merchantAddress: raw.merchant_address,
|
|
1080
|
+
payerAddress: raw.payer_address,
|
|
1081
|
+
settlementAddress: raw.settlement_address,
|
|
1082
|
+
tokenSymbol: raw.token_symbol,
|
|
1083
|
+
tokenAddress: raw.token_address,
|
|
1084
|
+
amountRaw: raw.amount_raw,
|
|
1085
|
+
amount: raw.amount_human,
|
|
1086
|
+
challengeId: raw.challenge_id,
|
|
1087
|
+
idempotencyKey: raw.idempotency_key,
|
|
1088
|
+
challengePayload: raw.challenge_payload,
|
|
1089
|
+
selectedPayment: raw.selected_payment,
|
|
1090
|
+
paymentProofHeaderName: raw.payment_proof_header_name,
|
|
1091
|
+
protocolReceiptHeaderName: raw.protocol_receipt_header_name,
|
|
1092
|
+
protocolReceiptPayload: raw.protocol_receipt_payload,
|
|
1093
|
+
merchantStatus: raw.merchant_status,
|
|
1094
|
+
confirmedAt: raw.confirmed_at,
|
|
1095
|
+
createdAt: raw.created_at,
|
|
1096
|
+
updatedAt: raw.updated_at
|
|
1097
|
+
};
|
|
1098
|
+
if ("payment_intent_id" in raw) {
|
|
1099
|
+
receipt.paymentIntentId = raw.payment_intent_id ?? null;
|
|
1003
1100
|
}
|
|
1101
|
+
if ("approval_request_id" in raw) {
|
|
1102
|
+
receipt.approvalRequestId = raw.approval_request_id ?? null;
|
|
1103
|
+
}
|
|
1104
|
+
return receipt;
|
|
1004
1105
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
const hasSpendable = allowances.some((a) => safeBigInt(a.remainingAtomic) > 0n);
|
|
1008
|
-
return hasSpendable ? "ready" : "needs_approval";
|
|
1009
|
-
}
|
|
1010
|
-
var MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
1011
|
-
var MCP_ACCEPT = "application/json, text/event-stream";
|
|
1012
|
-
var MCP_CLIENT_INFO = { name: "haven-sdk", version: "1" };
|
|
1013
|
-
var MERCHANT_BODY_SNIPPET_LIMIT = 1e3;
|
|
1106
|
+
|
|
1107
|
+
// src/payment-state.ts
|
|
1014
1108
|
var PAYMENT_STATE_STATUS_CODES = {
|
|
1015
1109
|
pending: 202,
|
|
1016
1110
|
pending_approval: 202,
|
|
@@ -1023,15 +1117,8 @@ var PAYMENT_STATE_STATUS_CODES = {
|
|
|
1023
1117
|
failed: 502,
|
|
1024
1118
|
rejected: 409
|
|
1025
1119
|
};
|
|
1026
|
-
function
|
|
1027
|
-
|
|
1028
|
-
if (network === "base-sepolia") return 84532;
|
|
1029
|
-
if (!network?.startsWith("eip155:")) return void 0;
|
|
1030
|
-
const chainId = Number(network.slice("eip155:".length));
|
|
1031
|
-
return Number.isFinite(chainId) ? chainId : void 0;
|
|
1032
|
-
}
|
|
1033
|
-
function chainIdOrNull(network) {
|
|
1034
|
-
return chainIdFromNetwork(network) ?? null;
|
|
1120
|
+
function paymentStateStatusCode(status, fallback = 502) {
|
|
1121
|
+
return PAYMENT_STATE_STATUS_CODES[status] ?? fallback;
|
|
1035
1122
|
}
|
|
1036
1123
|
function phaseForStatus(status) {
|
|
1037
1124
|
if (status === "pending_signature") return AgentPaymentPhase.AgentSignatureRequired;
|
|
@@ -1074,69 +1161,1484 @@ function messageForState(label, status, paymentId, nextAction) {
|
|
|
1074
1161
|
}
|
|
1075
1162
|
return `${label} is ${status}; next_action=${nextAction} (payment_id: ${paymentId}).`;
|
|
1076
1163
|
}
|
|
1077
|
-
function
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
const
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1164
|
+
function paymentStateFromRaw(label, raw) {
|
|
1165
|
+
if (!raw.payment_id || !raw.status) return null;
|
|
1166
|
+
const phase = raw.phase ?? phaseForStatus(raw.status);
|
|
1167
|
+
const nextAction = raw.next_action ?? nextActionForStatus(raw.status);
|
|
1168
|
+
if (!phase || !nextAction) return null;
|
|
1169
|
+
const amount = raw.amount ?? raw.requested ?? "";
|
|
1170
|
+
const token = raw.token ?? "";
|
|
1171
|
+
const message = raw.message ?? raw.error ?? messageForState(label, raw.status, raw.payment_id, nextAction);
|
|
1172
|
+
return {
|
|
1173
|
+
paymentId: raw.payment_id,
|
|
1174
|
+
kind: raw.kind === "payment_intent" ? "payment_intent" : "approval_request",
|
|
1175
|
+
rail: raw.rail ?? "direct",
|
|
1176
|
+
status: raw.status === "pending" ? "pending_approval" : raw.status,
|
|
1177
|
+
phase,
|
|
1178
|
+
nextAction,
|
|
1179
|
+
amount,
|
|
1180
|
+
token,
|
|
1181
|
+
resourceUrl: raw.resource_url ?? null,
|
|
1182
|
+
merchantAddress: raw.merchant_address ?? raw.merchant_to ?? null,
|
|
1183
|
+
txHash: raw.tx_hash ?? null,
|
|
1184
|
+
expiresAt: raw.expires_at ?? "",
|
|
1185
|
+
chainId: raw.chain_id ?? 0,
|
|
1186
|
+
message,
|
|
1187
|
+
amountAtomic: raw.amount_atomic ?? raw.x402?.amount_atomic ?? raw.mpp?.amount_atomic ?? null,
|
|
1188
|
+
asset: raw.asset ?? raw.x402?.asset ?? raw.mpp?.asset ?? null,
|
|
1189
|
+
network: raw.network ?? raw.x402?.network ?? raw.mpp?.network ?? null,
|
|
1190
|
+
description: raw.description ?? raw.x402?.description ?? raw.mpp?.description ?? null,
|
|
1191
|
+
idempotencyKey: raw.idempotency_key ?? raw.x402?.idempotency_key ?? raw.mpp?.idempotency_key ?? null,
|
|
1192
|
+
x402: raw.x402 ? {
|
|
1193
|
+
amountAtomic: raw.x402.amount_atomic ?? raw.amount_atomic ?? null,
|
|
1194
|
+
asset: raw.x402.asset ?? raw.asset ?? null,
|
|
1195
|
+
network: raw.x402.network ?? raw.network ?? null,
|
|
1196
|
+
resourceUrl: raw.x402.resource_url ?? raw.resource_url ?? null,
|
|
1197
|
+
merchantAddress: raw.x402.merchant_address ?? raw.merchant_address ?? raw.merchant_to ?? null,
|
|
1198
|
+
description: raw.x402.description ?? raw.description ?? null,
|
|
1199
|
+
idempotencyKey: raw.x402.idempotency_key ?? raw.idempotency_key ?? null
|
|
1200
|
+
} : void 0,
|
|
1201
|
+
mpp: raw.mpp ? {
|
|
1202
|
+
amountAtomic: raw.mpp.amount_atomic ?? raw.amount_atomic ?? null,
|
|
1203
|
+
asset: raw.mpp.asset ?? raw.asset ?? null,
|
|
1204
|
+
network: raw.mpp.network ?? raw.network ?? null,
|
|
1205
|
+
resourceUrl: raw.mpp.resource_url ?? raw.resource_url ?? null,
|
|
1206
|
+
merchantAddress: raw.mpp.merchant_address ?? raw.merchant_address ?? raw.merchant_to ?? null,
|
|
1207
|
+
description: raw.mpp.description ?? raw.description ?? null,
|
|
1208
|
+
idempotencyKey: raw.mpp.idempotency_key ?? raw.idempotency_key ?? null,
|
|
1209
|
+
challengeId: raw.mpp.challenge_id ?? raw.challenge_id ?? null
|
|
1210
|
+
} : void 0
|
|
1211
|
+
};
|
|
1098
1212
|
}
|
|
1099
|
-
function
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1213
|
+
function throwPaymentStateError(label, raw) {
|
|
1214
|
+
const statusCode = paymentStateStatusCode(raw.status);
|
|
1215
|
+
const state = paymentStateFromRaw(label, raw);
|
|
1216
|
+
if (state) {
|
|
1217
|
+
throw new HavenPaymentStateError(state.message, statusCode, state, raw);
|
|
1104
1218
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1219
|
+
if (raw.status === "pending_approval") {
|
|
1220
|
+
throw new HavenApiError(
|
|
1221
|
+
`${label} exceeds the on-chain allowance and was queued for owner approval (payment_id: ${raw.payment_id}).`,
|
|
1222
|
+
statusCode,
|
|
1223
|
+
raw
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
if (raw.status === "expired") {
|
|
1227
|
+
throw new HavenApiError(
|
|
1228
|
+
`${label} expired before it could be completed (payment_id: ${raw.payment_id}).`,
|
|
1229
|
+
statusCode,
|
|
1230
|
+
raw
|
|
1231
|
+
);
|
|
1112
1232
|
}
|
|
1233
|
+
const paymentId = raw.payment_id ? ` (payment_id: ${raw.payment_id})` : "";
|
|
1234
|
+
const message = raw.error ?? `${label} ${raw.status}${paymentId}`;
|
|
1235
|
+
throw new HavenApiError(message, statusCode, raw);
|
|
1113
1236
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1237
|
+
|
|
1238
|
+
// src/mcp-merchant-transport.ts
|
|
1239
|
+
var DEFAULT_MERCHANT_TIMEOUT = 3e5;
|
|
1240
|
+
var MCP_NOTIFICATION_TIMEOUT = 1e4;
|
|
1241
|
+
var MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
1242
|
+
var MCP_ACCEPT = "application/json, text/event-stream";
|
|
1243
|
+
var MCP_CLIENT_INFO = { name: "haven-sdk", version: "1" };
|
|
1244
|
+
var McpMerchantTransport = class {
|
|
1245
|
+
merchantTimeout;
|
|
1246
|
+
fetchImpl;
|
|
1247
|
+
requestId = 0;
|
|
1248
|
+
constructor(options = {}) {
|
|
1249
|
+
this.merchantTimeout = options.merchantTimeout ?? DEFAULT_MERCHANT_TIMEOUT;
|
|
1250
|
+
this.fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
1251
|
+
}
|
|
1252
|
+
/** Fetch a merchant with a settlement-sized timeout and caller cancellation. */
|
|
1253
|
+
async fetch(url, init = {}, timeoutMs = this.merchantTimeout) {
|
|
1254
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
1255
|
+
const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
|
|
1119
1256
|
try {
|
|
1120
|
-
|
|
1257
|
+
return await this.fetchImpl(url, { ...init, signal });
|
|
1258
|
+
} catch (err) {
|
|
1259
|
+
if (timeoutSignal.aborted) {
|
|
1260
|
+
throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${url}`);
|
|
1261
|
+
}
|
|
1262
|
+
throw err;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Establish an MCP Streamable-HTTP session. Any handshake failure degrades
|
|
1267
|
+
* to `undefined`, allowing the caller to fall back to plain x402.
|
|
1268
|
+
*/
|
|
1269
|
+
async initialize(url, init, wallet) {
|
|
1270
|
+
try {
|
|
1271
|
+
const headers = new Headers(init?.headers);
|
|
1272
|
+
headers.set("Content-Type", "application/json");
|
|
1273
|
+
headers.set("Accept", MCP_ACCEPT);
|
|
1274
|
+
if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
|
|
1275
|
+
const response = await this.fetch(url, {
|
|
1276
|
+
method: "POST",
|
|
1277
|
+
headers,
|
|
1278
|
+
body: JSON.stringify({
|
|
1279
|
+
jsonrpc: "2.0",
|
|
1280
|
+
id: ++this.requestId,
|
|
1281
|
+
method: "initialize",
|
|
1282
|
+
params: {
|
|
1283
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
1284
|
+
capabilities: {},
|
|
1285
|
+
clientInfo: MCP_CLIENT_INFO
|
|
1286
|
+
}
|
|
1287
|
+
})
|
|
1288
|
+
});
|
|
1289
|
+
if (!response.ok) return void 0;
|
|
1290
|
+
const sessionId = response.headers.get("mcp-session-id");
|
|
1291
|
+
if (!sessionId) return void 0;
|
|
1292
|
+
const message = await this.readMessage(response);
|
|
1293
|
+
if (message && "error" in message) return void 0;
|
|
1294
|
+
await this.notifyInitialized(url, init, sessionId, wallet);
|
|
1295
|
+
return sessionId;
|
|
1121
1296
|
} catch {
|
|
1297
|
+
return void 0;
|
|
1122
1298
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1299
|
+
}
|
|
1300
|
+
/** Add the MCP session and response-content negotiation headers. */
|
|
1301
|
+
withSessionHeaders(init, sessionId) {
|
|
1302
|
+
const headers = new Headers(init?.headers);
|
|
1303
|
+
headers.set("mcp-session-id", sessionId);
|
|
1304
|
+
headers.set("Accept", MCP_ACCEPT);
|
|
1305
|
+
return { ...init, headers };
|
|
1306
|
+
}
|
|
1307
|
+
/** Read one JSON-RPC message from a JSON or SSE response without consuming it. */
|
|
1308
|
+
async readMessage(response) {
|
|
1309
|
+
let text;
|
|
1310
|
+
try {
|
|
1311
|
+
text = await response.clone().text();
|
|
1312
|
+
} catch {
|
|
1313
|
+
return void 0;
|
|
1129
1314
|
}
|
|
1130
|
-
if (
|
|
1131
|
-
|
|
1315
|
+
if ((response.headers.get("content-type") ?? "").includes("text/event-stream")) {
|
|
1316
|
+
return selectJsonRpcResult(parseSseJsonRpcMessages(text));
|
|
1317
|
+
}
|
|
1318
|
+
try {
|
|
1319
|
+
return JSON.parse(text);
|
|
1320
|
+
} catch {
|
|
1321
|
+
return void 0;
|
|
1132
1322
|
}
|
|
1133
1323
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1324
|
+
/**
|
|
1325
|
+
* Collapse an MCP SSE response to the JSON-RPC result. Non-SSE and
|
|
1326
|
+
* unparseable responses pass through with their original body untouched.
|
|
1327
|
+
*/
|
|
1328
|
+
async surfaceResult(response) {
|
|
1329
|
+
if (!(response.headers.get("content-type") ?? "").includes("text/event-stream")) {
|
|
1330
|
+
return response;
|
|
1331
|
+
}
|
|
1332
|
+
let text;
|
|
1333
|
+
try {
|
|
1334
|
+
text = await response.clone().text();
|
|
1335
|
+
} catch {
|
|
1336
|
+
return response;
|
|
1337
|
+
}
|
|
1338
|
+
const message = selectJsonRpcResult(parseSseJsonRpcMessages(text));
|
|
1339
|
+
if (!message) return response;
|
|
1340
|
+
const body = "result" in message ? message.result : message;
|
|
1341
|
+
const headers = new Headers(response.headers);
|
|
1342
|
+
headers.set("content-type", "application/json");
|
|
1343
|
+
headers.delete("content-length");
|
|
1344
|
+
headers.delete("mcp-session-id");
|
|
1345
|
+
return new Response(JSON.stringify(body), {
|
|
1346
|
+
status: response.status,
|
|
1347
|
+
statusText: response.statusText,
|
|
1348
|
+
headers
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
/** Detect MCP transport from the URL or Coinbase Bazaar extension. */
|
|
1352
|
+
async detect(url, paymentRequired, response) {
|
|
1353
|
+
if (isMcpUrl(url)) return { handshakeRequired: true, source: "path" };
|
|
1354
|
+
if (paymentRequired.extensions?.bazaar != null) {
|
|
1355
|
+
return { handshakeRequired: true, source: "bazaar" };
|
|
1356
|
+
}
|
|
1357
|
+
if (await responseHasBazaarExtension(response)) {
|
|
1358
|
+
return { handshakeRequired: true, source: "bazaar" };
|
|
1359
|
+
}
|
|
1360
|
+
return void 0;
|
|
1361
|
+
}
|
|
1362
|
+
/** Identify the conventional Streamable-HTTP MCP path without probing it. */
|
|
1363
|
+
isMcpUrl(url) {
|
|
1364
|
+
return isMcpUrl(url);
|
|
1365
|
+
}
|
|
1366
|
+
/** Detect Bazaar metadata without consuming the merchant response. */
|
|
1367
|
+
hasBazaarExtension(response) {
|
|
1368
|
+
return responseHasBazaarExtension(response);
|
|
1369
|
+
}
|
|
1370
|
+
/** Deliver an already-signed x402 header without changing the caller body. */
|
|
1371
|
+
async deliverPayment(url, init, paymentHeader) {
|
|
1372
|
+
const headers = new Headers(init?.headers);
|
|
1373
|
+
headers.set("X-PAYMENT", paymentHeader);
|
|
1374
|
+
return this.fetch(url, { ...init, headers });
|
|
1375
|
+
}
|
|
1376
|
+
async notifyInitialized(url, init, sessionId, wallet) {
|
|
1377
|
+
try {
|
|
1378
|
+
const headers = new Headers(init?.headers);
|
|
1379
|
+
headers.set("Content-Type", "application/json");
|
|
1380
|
+
headers.set("Accept", MCP_ACCEPT);
|
|
1381
|
+
headers.set("mcp-session-id", sessionId);
|
|
1382
|
+
if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
|
|
1383
|
+
await this.fetch(
|
|
1384
|
+
url,
|
|
1385
|
+
{
|
|
1386
|
+
method: "POST",
|
|
1387
|
+
headers,
|
|
1388
|
+
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
|
|
1389
|
+
},
|
|
1390
|
+
MCP_NOTIFICATION_TIMEOUT
|
|
1391
|
+
);
|
|
1392
|
+
} catch {
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
async function captureMerchantResponse(response) {
|
|
1397
|
+
const merchant_body = await response.text().catch(() => "");
|
|
1398
|
+
return {
|
|
1399
|
+
merchant_status: response.status,
|
|
1400
|
+
merchant_status_text: response.statusText,
|
|
1401
|
+
merchant_headers: Object.fromEntries(response.headers.entries()),
|
|
1402
|
+
merchant_body
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
function isMcpUrl(url) {
|
|
1406
|
+
try {
|
|
1407
|
+
return new URL(url).pathname.replace(/\/+$/, "").endsWith("/mcp");
|
|
1408
|
+
} catch {
|
|
1409
|
+
return /\/mcp(?:[/?#]|$)/.test(url);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
async function responseHasBazaarExtension(response) {
|
|
1413
|
+
try {
|
|
1414
|
+
const body = await response.clone().json();
|
|
1415
|
+
return body?.extensions?.bazaar != null;
|
|
1416
|
+
} catch {
|
|
1417
|
+
return false;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
function parseSseJsonRpcMessages(text) {
|
|
1421
|
+
const messages = [];
|
|
1422
|
+
let dataLines = [];
|
|
1423
|
+
const flush = () => {
|
|
1424
|
+
if (dataLines.length === 0) return;
|
|
1425
|
+
try {
|
|
1426
|
+
messages.push(JSON.parse(dataLines.join("\n")));
|
|
1427
|
+
} catch {
|
|
1428
|
+
}
|
|
1429
|
+
dataLines = [];
|
|
1430
|
+
};
|
|
1431
|
+
for (const line of text.split(/\r?\n/)) {
|
|
1432
|
+
if (line === "") {
|
|
1433
|
+
flush();
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
1437
|
+
}
|
|
1438
|
+
flush();
|
|
1439
|
+
return messages;
|
|
1440
|
+
}
|
|
1441
|
+
function selectJsonRpcResult(messages) {
|
|
1442
|
+
return messages.find((message) => "result" in message || "error" in message) ?? messages.at(-1);
|
|
1443
|
+
}
|
|
1444
|
+
var RECEIPT_VERSION = "haven-receipt-1";
|
|
1445
|
+
function defaultRecover(hash, signature) {
|
|
1446
|
+
return ethers.recoverAddress(hash, signature);
|
|
1447
|
+
}
|
|
1448
|
+
function verifyPaymentReceipt(receipt, recover = defaultRecover) {
|
|
1449
|
+
const { delegate, signHash: signHash2, signature } = receipt.authorization;
|
|
1450
|
+
if (!signature) return { verified: false, reason: "missing_signature" };
|
|
1451
|
+
let recovered;
|
|
1452
|
+
try {
|
|
1453
|
+
recovered = recover(signHash2, signature);
|
|
1454
|
+
} catch {
|
|
1455
|
+
return { verified: false, reason: "bad_signature" };
|
|
1456
|
+
}
|
|
1457
|
+
if (recovered.toLowerCase() !== delegate.toLowerCase()) {
|
|
1458
|
+
return { verified: false, reason: "signer_mismatch", recoveredSigner: recovered };
|
|
1459
|
+
}
|
|
1460
|
+
return { verified: true, recoveredSigner: recovered };
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// src/account-reads.ts
|
|
1464
|
+
function safeBigInt(value) {
|
|
1465
|
+
try {
|
|
1466
|
+
return BigInt(value);
|
|
1467
|
+
} catch {
|
|
1468
|
+
return 0n;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
function formatAtomicAmount(atomic, decimals) {
|
|
1472
|
+
if (atomic < 0n) return "0.0";
|
|
1473
|
+
const value = atomic.toString().padStart(decimals + 1, "0");
|
|
1474
|
+
const whole = value.slice(0, value.length - decimals) || "0";
|
|
1475
|
+
const fraction = value.slice(value.length - decimals).replace(/0+$/, "") || "0";
|
|
1476
|
+
return `${whole}.${fraction}`;
|
|
1477
|
+
}
|
|
1478
|
+
function deriveReadiness(status, allowances) {
|
|
1479
|
+
if (status !== "active") return "revoked";
|
|
1480
|
+
return allowances.some((allowance) => safeBigInt(allowance.remainingAtomic) > 0n) ? "ready" : "needs_approval";
|
|
1481
|
+
}
|
|
1482
|
+
var AccountReads = class {
|
|
1483
|
+
transport;
|
|
1484
|
+
getPaymentStatus;
|
|
1485
|
+
agentInFlight = null;
|
|
1486
|
+
constructor(options) {
|
|
1487
|
+
this.transport = options.transport;
|
|
1488
|
+
this.getPaymentStatus = options.getPaymentStatus;
|
|
1489
|
+
}
|
|
1490
|
+
async getAgent() {
|
|
1491
|
+
if (this.agentInFlight) return this.agentInFlight;
|
|
1492
|
+
const request = this.fetchAgent();
|
|
1493
|
+
this.agentInFlight = request;
|
|
1494
|
+
request.finally(() => {
|
|
1495
|
+
this.agentInFlight = null;
|
|
1496
|
+
}).catch(() => {
|
|
1497
|
+
});
|
|
1498
|
+
return request;
|
|
1499
|
+
}
|
|
1500
|
+
async getAgentSummary() {
|
|
1501
|
+
const [agent, allowanceSummary] = await Promise.all([this.getAgent(), this.getAllowances()]);
|
|
1502
|
+
const allowances = allowanceSummary.allowances.map((allowance) => {
|
|
1503
|
+
const token = resolveTokenFromAddress(allowance.tokenAddress);
|
|
1504
|
+
const remainingDisplay = token ? `${formatAtomicAmount(safeBigInt(allowance.onchain.remaining), token.decimals)} ${allowance.tokenSymbol}` : `${allowance.onchain.remaining} ${allowance.tokenSymbol} (atomic; unknown decimals)`;
|
|
1505
|
+
return {
|
|
1506
|
+
tokenSymbol: allowance.tokenSymbol,
|
|
1507
|
+
remainingAtomic: allowance.onchain.remaining,
|
|
1508
|
+
remainingDisplay,
|
|
1509
|
+
configuredAmount: allowance.configuredAmount,
|
|
1510
|
+
resetPeriodMin: allowance.resetPeriodMin,
|
|
1511
|
+
isResetPending: allowance.onchain.isResetPending
|
|
1512
|
+
};
|
|
1513
|
+
});
|
|
1514
|
+
const readiness = deriveReadiness(agent.status, allowances);
|
|
1515
|
+
return { ...agent, readiness, spend_authority_readiness: readiness, allowances };
|
|
1516
|
+
}
|
|
1517
|
+
async getAllowances() {
|
|
1518
|
+
const raw = await this.transport.get("/machine-payments/allowances");
|
|
1519
|
+
return {
|
|
1520
|
+
agentId: raw.agent_id,
|
|
1521
|
+
safeAddress: raw.safe_address,
|
|
1522
|
+
delegateAddress: raw.delegate_address,
|
|
1523
|
+
chainId: raw.chain_id,
|
|
1524
|
+
allowances: raw.allowances.map((allowance) => ({
|
|
1525
|
+
id: allowance.id,
|
|
1526
|
+
tokenAddress: allowance.token_address,
|
|
1527
|
+
tokenSymbol: allowance.token_symbol,
|
|
1528
|
+
configuredAmount: allowance.configured_amount,
|
|
1529
|
+
resetPeriodMin: allowance.reset_period_min,
|
|
1530
|
+
onchain: {
|
|
1531
|
+
amount: allowance.onchain.amount,
|
|
1532
|
+
spent: allowance.onchain.spent,
|
|
1533
|
+
remaining: allowance.onchain.remaining,
|
|
1534
|
+
effectiveSpent: allowance.onchain.effective_spent,
|
|
1535
|
+
resetTimeMin: allowance.onchain.reset_time_min,
|
|
1536
|
+
lastResetMin: allowance.onchain.last_reset_min,
|
|
1537
|
+
nonce: allowance.onchain.nonce,
|
|
1538
|
+
isResetPending: allowance.onchain.is_reset_pending,
|
|
1539
|
+
remainingIsFromChain: allowance.onchain.remaining_is_from_chain
|
|
1540
|
+
}
|
|
1541
|
+
}))
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
async getPostPurchaseAllowanceSummary(paymentId) {
|
|
1545
|
+
const unavailable = (detail, payment2 = null) => ({
|
|
1546
|
+
payment: payment2,
|
|
1547
|
+
allowance: null,
|
|
1548
|
+
warnings: [{
|
|
1549
|
+
code: AgentPaymentWarningCode.AllowanceCheckUnavailable,
|
|
1550
|
+
message: `Could not read the post-purchase allowance/budget for payment ${paymentId} (${detail}). The payment itself succeeded \u2014 the on-chain policy remains the actual spend gate; this only affects the remaining-budget figure reported here.`
|
|
1551
|
+
}]
|
|
1552
|
+
});
|
|
1553
|
+
const [statusResult, agentResult, allowanceResult] = await Promise.allSettled([
|
|
1554
|
+
this.getPaymentStatus(paymentId),
|
|
1555
|
+
this.getAgent(),
|
|
1556
|
+
this.getAllowances()
|
|
1557
|
+
]);
|
|
1558
|
+
if (statusResult.status === "rejected") {
|
|
1559
|
+
return unavailable(statusResult.reason instanceof Error ? statusResult.reason.message : String(statusResult.reason));
|
|
1560
|
+
}
|
|
1561
|
+
const payment = statusResult.value;
|
|
1562
|
+
if (agentResult.status === "rejected") {
|
|
1563
|
+
return unavailable(agentResult.reason instanceof Error ? agentResult.reason.message : String(agentResult.reason), payment);
|
|
1564
|
+
}
|
|
1565
|
+
if (allowanceResult.status === "rejected") {
|
|
1566
|
+
return unavailable(allowanceResult.reason instanceof Error ? allowanceResult.reason.message : String(allowanceResult.reason), payment);
|
|
1567
|
+
}
|
|
1568
|
+
try {
|
|
1569
|
+
const tokenAddress = payment.asset ?? payment.x402?.asset ?? null;
|
|
1570
|
+
if (!tokenAddress) return unavailable("the settled payment does not carry a resolvable token address", payment);
|
|
1571
|
+
const match = allowanceResult.value.allowances.find(
|
|
1572
|
+
(allowance) => allowance.tokenAddress.toLowerCase() === tokenAddress.toLowerCase()
|
|
1573
|
+
);
|
|
1574
|
+
if (!match) return unavailable("no allowance/budget row matches the settled token", payment);
|
|
1575
|
+
const token = resolveTokenFromAddress(match.tokenAddress);
|
|
1576
|
+
const remainingDisplay = token ? `${formatAtomicAmount(safeBigInt(match.onchain.remaining), token.decimals)} ${match.tokenSymbol}` : void 0;
|
|
1577
|
+
const rail = agentResult.value.executionRail;
|
|
1578
|
+
return {
|
|
1579
|
+
payment,
|
|
1580
|
+
allowance: {
|
|
1581
|
+
rail,
|
|
1582
|
+
remaining_atomic: match.onchain.remaining,
|
|
1583
|
+
...remainingDisplay ? { remaining_display: remainingDisplay } : {},
|
|
1584
|
+
token_symbol: match.tokenSymbol,
|
|
1585
|
+
token_address: match.tokenAddress,
|
|
1586
|
+
reset_period: match.resetPeriodMin,
|
|
1587
|
+
source: rail === "delegation" ? "active_delegations" : "allowance_module"
|
|
1588
|
+
},
|
|
1589
|
+
warnings: []
|
|
1590
|
+
};
|
|
1591
|
+
} catch (error) {
|
|
1592
|
+
return unavailable(error instanceof Error ? error.message : String(error));
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
async listReceipts(options = {}) {
|
|
1596
|
+
const query = options.limit ? `?limit=${encodeURIComponent(String(options.limit))}` : "";
|
|
1597
|
+
const raw = await this.transport.get(`/machine-payments/receipts${query}`);
|
|
1598
|
+
return raw.receipts.map(mapPaymentReceipt);
|
|
1599
|
+
}
|
|
1600
|
+
async getReceipt(paymentId) {
|
|
1601
|
+
const { receipt } = await this.transport.get(`/payments/${paymentId}/receipt`);
|
|
1602
|
+
return { receipt, verification: verifyPaymentReceipt(receipt) };
|
|
1603
|
+
}
|
|
1604
|
+
async fetchAgent() {
|
|
1605
|
+
const raw = await this.transport.get("/machine-payments/agent");
|
|
1606
|
+
return {
|
|
1607
|
+
id: raw.id,
|
|
1608
|
+
name: raw.name,
|
|
1609
|
+
status: raw.status,
|
|
1610
|
+
safeAddress: raw.safe_address,
|
|
1611
|
+
delegateAddress: raw.delegate_address,
|
|
1612
|
+
chainId: raw.chain_id,
|
|
1613
|
+
executionRail: raw.execution_rail === "delegation" ? "delegation" : "legacy"
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
function createJsonRpcProvider(url) {
|
|
1618
|
+
return new ethers.JsonRpcProvider(url);
|
|
1619
|
+
}
|
|
1620
|
+
function createWallet(privateKey, provider) {
|
|
1621
|
+
return new ethers.Wallet(privateKey, provider);
|
|
1622
|
+
}
|
|
1623
|
+
function createErc20Contract(address, abi, runner) {
|
|
1624
|
+
return new ethers.Contract(address, abi, runner);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// src/delegate-sweep.ts
|
|
1628
|
+
var DelegateSweepApi = class {
|
|
1629
|
+
constructor(options) {
|
|
1630
|
+
this.options = options;
|
|
1631
|
+
}
|
|
1632
|
+
options;
|
|
1633
|
+
async sweepDelegate() {
|
|
1634
|
+
if (!this.options.delegateKey) throw new HavenSigningError("delegateKey is required for sweepDelegate.");
|
|
1635
|
+
const agent = await this.options.getAgent();
|
|
1636
|
+
if (!agent.delegateAddress) throw new HavenApiError("Agent has no delegate address.", 422);
|
|
1637
|
+
const rpcUrl = this.options.chainRpcs[agent.chainId];
|
|
1638
|
+
if (!rpcUrl) throw new HavenApiError(`chainRpcs[${agent.chainId}] must be configured to sweep the delegate wallet.`, 422);
|
|
1639
|
+
const provider = createJsonRpcProvider(rpcUrl);
|
|
1640
|
+
const wallet = createWallet(this.options.delegateKey, provider);
|
|
1641
|
+
const transfers = [];
|
|
1642
|
+
if (isSweepableChain(agent.chainId)) {
|
|
1643
|
+
const contract = createErc20Contract(sweepUsdcAddress(agent.chainId), ["function balanceOf(address) view returns (uint256)", "function transfer(address to, uint256 amount) returns (bool)"], wallet);
|
|
1644
|
+
const balance2 = await contract.balanceOf(agent.delegateAddress);
|
|
1645
|
+
if (balance2 > 0n) {
|
|
1646
|
+
const tx = await contract.transfer(agent.safeAddress, balance2);
|
|
1647
|
+
const receipt = await tx.wait(1);
|
|
1648
|
+
const txHash = receipt?.hash ?? tx.hash;
|
|
1649
|
+
transfers.push({ asset: "USDC", amount: format(balance2, 6), amountAtomic: balance2.toString(), txHash, explorerUrl: this.options.buildExplorerUrl(agent.chainId, txHash) });
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
const balance = await provider.getBalance(agent.delegateAddress);
|
|
1653
|
+
if (balance > 0n) {
|
|
1654
|
+
const fee = await provider.getFeeData();
|
|
1655
|
+
const send = balance - (fee.maxFeePerGas ?? fee.gasPrice ?? 1000000n) * 21000n * 2n;
|
|
1656
|
+
if (send > 0n) {
|
|
1657
|
+
const tx = await wallet.sendTransaction({ to: agent.safeAddress, value: send });
|
|
1658
|
+
const receipt = await tx.wait(1);
|
|
1659
|
+
const txHash = receipt?.hash ?? tx.hash;
|
|
1660
|
+
transfers.push({ asset: "ETH", amount: format(send, 18), amountAtomic: send.toString(), txHash, explorerUrl: this.options.buildExplorerUrl(agent.chainId, txHash) });
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
return { fromAddress: agent.delegateAddress, toAddress: agent.safeAddress, chainId: agent.chainId, transfers };
|
|
1664
|
+
}
|
|
1665
|
+
prepareSweep() {
|
|
1666
|
+
return this.options.transport.post("/machine-payments/sweep/prepare", {});
|
|
1667
|
+
}
|
|
1668
|
+
submitSweep(authorization, signature) {
|
|
1669
|
+
return this.options.transport.post("/machine-payments/sweep/submit", { authorization, signature });
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
function format(value, decimals) {
|
|
1673
|
+
const raw = value.toString().padStart(decimals + 1, "0");
|
|
1674
|
+
return `${raw.slice(0, -decimals) || "0"}.${raw.slice(-decimals).replace(/0+$/, "") || "0"}`;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// src/x402-protocol.ts
|
|
1678
|
+
var CHAIN_EXPLORER_TX = {
|
|
1679
|
+
100: "https://gnosisscan.io/tx",
|
|
1680
|
+
8453: "https://basescan.org/tx"
|
|
1681
|
+
};
|
|
1682
|
+
function buildExplorerUrl(chainId, txHash) {
|
|
1683
|
+
const base = CHAIN_EXPLORER_TX[chainId ?? 8453] ?? CHAIN_EXPLORER_TX[8453];
|
|
1684
|
+
return `${base}/${txHash}`;
|
|
1685
|
+
}
|
|
1686
|
+
function explorerUrlOrEmpty(chainId, txHash) {
|
|
1687
|
+
return txHash ? buildExplorerUrl(chainId, txHash) : "";
|
|
1688
|
+
}
|
|
1689
|
+
function chainIdFromNetwork(network) {
|
|
1690
|
+
if (network === "base") return 8453;
|
|
1691
|
+
if (network === "base-sepolia") return 84532;
|
|
1692
|
+
if (!network?.startsWith("eip155:")) return void 0;
|
|
1693
|
+
const chainId = Number(network.slice("eip155:".length));
|
|
1694
|
+
return Number.isFinite(chainId) ? chainId : void 0;
|
|
1695
|
+
}
|
|
1696
|
+
function chainIdOrNull(network) {
|
|
1697
|
+
return chainIdFromNetwork(network) ?? null;
|
|
1698
|
+
}
|
|
1699
|
+
function sameAddress3(a, b) {
|
|
1700
|
+
return Boolean(a && b && a.toLowerCase() === b.toLowerCase());
|
|
1701
|
+
}
|
|
1702
|
+
function decimalFromUsdcAtomic(value) {
|
|
1703
|
+
const amount = BigInt(value);
|
|
1704
|
+
const whole = amount / 1000000n;
|
|
1705
|
+
const fraction = (amount % 1000000n).toString().padStart(6, "0").replace(/0+$/, "");
|
|
1706
|
+
return fraction ? `${whole}.${fraction}` : whole.toString();
|
|
1707
|
+
}
|
|
1708
|
+
function normalizeDecimal(value) {
|
|
1709
|
+
if (!value.includes(".")) return value.replace(/^0+(?=\d)/, "") || "0";
|
|
1710
|
+
const [whole, fraction = ""] = value.split(".");
|
|
1711
|
+
const normalizedWhole = whole.replace(/^0+(?=\d)/, "") || "0";
|
|
1712
|
+
const normalizedFraction = fraction.replace(/0+$/, "");
|
|
1713
|
+
return normalizedFraction ? `${normalizedWhole}.${normalizedFraction}` : normalizedWhole;
|
|
1714
|
+
}
|
|
1715
|
+
function x402PayerAddress(delegateAddress, x402Wallet) {
|
|
1716
|
+
return delegateAddress ?? x402Wallet;
|
|
1717
|
+
}
|
|
1718
|
+
function withX402Wallet(init, wallet) {
|
|
1719
|
+
if (!wallet) return init;
|
|
1720
|
+
const headers = new Headers(init?.headers);
|
|
1721
|
+
if (!headers.has("x402-wallet")) {
|
|
1722
|
+
headers.set("x402-wallet", wallet);
|
|
1723
|
+
}
|
|
1724
|
+
return {
|
|
1725
|
+
...init,
|
|
1726
|
+
headers
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
function snapshotRequestBody(body) {
|
|
1730
|
+
if (body == null) return void 0;
|
|
1731
|
+
if (typeof body === "string") return body;
|
|
1732
|
+
if (body instanceof URLSearchParams) return body.toString();
|
|
1733
|
+
throw new HavenApiError(
|
|
1734
|
+
"Quote helpers can only capture resumable request bodies that are strings or URLSearchParams. For streams, blobs, or binary bodies, preserve the original request yourself and call the matching resume method with fresh init.",
|
|
1735
|
+
400
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
function snapshotX402Request(url, init) {
|
|
1739
|
+
return {
|
|
1740
|
+
url,
|
|
1741
|
+
method: init?.method ?? "GET",
|
|
1742
|
+
headers: Array.from(new Headers(init?.headers).entries()),
|
|
1743
|
+
body: snapshotRequestBody(init?.body)
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
function requestInitFromSnapshot(request) {
|
|
1747
|
+
return {
|
|
1748
|
+
method: request.method,
|
|
1749
|
+
headers: request.headers,
|
|
1750
|
+
body: request.body
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
function buildX402Quote(paymentRequired, request, idempotencyKey, mcpTransport) {
|
|
1754
|
+
const option = selectStandardPaymentOption(paymentRequired.accepts);
|
|
1755
|
+
if (!option) {
|
|
1756
|
+
throw new HavenApiError(
|
|
1757
|
+
"No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC.",
|
|
1758
|
+
400
|
|
1759
|
+
);
|
|
1760
|
+
}
|
|
1761
|
+
const token = resolveTokenFromAddress(option.asset, option.network);
|
|
1762
|
+
return {
|
|
1763
|
+
rail: "x402",
|
|
1764
|
+
idempotencyKey: idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option),
|
|
1765
|
+
paymentRequired,
|
|
1766
|
+
accepted: option,
|
|
1767
|
+
request,
|
|
1768
|
+
...mcpTransport ? { mcpTransport } : {},
|
|
1769
|
+
resourceUrl: paymentRequired.resource.url,
|
|
1770
|
+
description: paymentRequired.resource.description ?? option.description ?? null,
|
|
1771
|
+
mimeType: paymentRequired.resource.mimeType ?? option.mimeType ?? null,
|
|
1772
|
+
amountAtomic: x402AuthorizationAmount(option),
|
|
1773
|
+
amount: decimalFromUsdcAtomic(x402AuthorizationAmount(option)),
|
|
1774
|
+
token: token?.symbol ?? "USDC",
|
|
1775
|
+
// #1351: null when the asset is unrecognised on this network — the
|
|
1776
|
+
// `token` fallback above is a LABEL, not evidence of 6 decimals, and a
|
|
1777
|
+
// human-denominated cap must fail closed rather than convert against a
|
|
1778
|
+
// guess. Same resolution as `token`, so the two never disagree.
|
|
1779
|
+
decimals: token?.decimals ?? null,
|
|
1780
|
+
asset: option.asset,
|
|
1781
|
+
network: option.network,
|
|
1782
|
+
chainId: chainIdOrNull(option.network),
|
|
1783
|
+
merchantAddress: option.payTo,
|
|
1784
|
+
maxTimeoutSeconds: option.maxTimeoutSeconds
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
function buildX402Receipt(input) {
|
|
1788
|
+
const fundingExplorerUrl = input.explorerUrl || explorerUrlOrEmpty(input.chainId, input.txHash);
|
|
1789
|
+
return {
|
|
1790
|
+
success: true,
|
|
1791
|
+
paymentId: input.paymentId,
|
|
1792
|
+
txHash: input.txHash,
|
|
1793
|
+
token: input.token,
|
|
1794
|
+
amount: input.amount,
|
|
1795
|
+
to: input.to,
|
|
1796
|
+
resourceUrl: input.resourceUrl,
|
|
1797
|
+
explorerUrl: input.explorerUrl,
|
|
1798
|
+
accepted: input.accepted,
|
|
1799
|
+
paymentHeader: input.paymentHeader,
|
|
1800
|
+
merchantTo: input.merchantTo ?? input.accepted.payTo,
|
|
1801
|
+
payer: input.payer,
|
|
1802
|
+
chainId: input.chainId,
|
|
1803
|
+
haven: {
|
|
1804
|
+
paymentId: input.paymentId,
|
|
1805
|
+
fundingTxHash: input.txHash,
|
|
1806
|
+
fundingExplorerUrl
|
|
1807
|
+
},
|
|
1808
|
+
merchant: {
|
|
1809
|
+
payTo: input.merchantTo ?? input.accepted.payTo
|
|
1810
|
+
},
|
|
1811
|
+
x402: {
|
|
1812
|
+
amount: x402AuthorizationAmount(input.accepted),
|
|
1813
|
+
token: input.token,
|
|
1814
|
+
network: input.accepted.network,
|
|
1815
|
+
asset: input.accepted.asset,
|
|
1816
|
+
resource: input.accepted.resource ?? input.resourceUrl
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
function buildX402ResumeState(input) {
|
|
1821
|
+
const token = resolveTokenFromAddress(input.accepted.asset, input.accepted.network);
|
|
1822
|
+
return {
|
|
1823
|
+
rail: "x402",
|
|
1824
|
+
paymentId: input.paymentId,
|
|
1825
|
+
idempotencyKey: input.idempotencyKey,
|
|
1826
|
+
paymentRequired: input.paymentRequired,
|
|
1827
|
+
accepted: input.accepted,
|
|
1828
|
+
url: input.request?.url ?? input.paymentRequired.resource.url,
|
|
1829
|
+
request: input.request,
|
|
1830
|
+
resourceUrl: input.paymentRequired.resource.url,
|
|
1831
|
+
description: input.paymentRequired.resource.description ?? input.accepted.description ?? null,
|
|
1832
|
+
amountAtomic: x402AuthorizationAmount(input.accepted),
|
|
1833
|
+
amount: decimalFromUsdcAtomic(x402AuthorizationAmount(input.accepted)),
|
|
1834
|
+
token: token?.symbol ?? "USDC",
|
|
1835
|
+
asset: input.accepted.asset,
|
|
1836
|
+
network: input.accepted.network,
|
|
1837
|
+
chainId: chainIdOrNull(input.accepted.network),
|
|
1838
|
+
merchantAddress: input.accepted.payTo
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
function attachX402ResumeState(err, paymentRequired, accepted, idempotencyKey, request) {
|
|
1842
|
+
if (!(err instanceof HavenPaymentStateError)) return;
|
|
1843
|
+
if (err.state.rail !== "x402") return;
|
|
1844
|
+
err.resumeState = buildX402ResumeState({
|
|
1845
|
+
paymentId: err.state.paymentId,
|
|
1846
|
+
paymentRequired,
|
|
1847
|
+
accepted,
|
|
1848
|
+
idempotencyKey,
|
|
1849
|
+
request
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
function attachResumeState(err, input) {
|
|
1853
|
+
attachX402ResumeState(
|
|
1854
|
+
err,
|
|
1855
|
+
input.paymentRequired,
|
|
1856
|
+
input.accepted,
|
|
1857
|
+
input.idempotencyKey,
|
|
1858
|
+
input.request
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
function assertCanResumeX402(status, paymentRequired, option) {
|
|
1862
|
+
if (status.rail !== "x402") {
|
|
1863
|
+
throw new HavenPaymentStateError(
|
|
1864
|
+
`Payment ${status.paymentId} is ${status.rail}, not x402.`,
|
|
1865
|
+
409,
|
|
1866
|
+
status
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
if (status.nextAction !== AgentPaymentNextAction.RetryOriginalX402Request) {
|
|
1870
|
+
throw new HavenPaymentStateError(status.message, paymentStateStatusCode(status.status, 409), status);
|
|
1871
|
+
}
|
|
1872
|
+
if (!status.txHash) {
|
|
1873
|
+
throw new HavenApiError(
|
|
1874
|
+
`x402 payment ${status.paymentId} is ready to retry but has no Haven transaction hash.`,
|
|
1875
|
+
502,
|
|
1876
|
+
status,
|
|
1877
|
+
status.paymentId
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
if (status.resourceUrl && status.resourceUrl !== paymentRequired.resource.url) {
|
|
1881
|
+
throw new HavenApiError(
|
|
1882
|
+
"x402 resume request does not match the approved resource URL.",
|
|
1883
|
+
409,
|
|
1884
|
+
{ status, paymentRequired },
|
|
1885
|
+
status.paymentId
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
if (status.merchantAddress && !sameAddress3(status.merchantAddress, option.payTo)) {
|
|
1889
|
+
throw new HavenApiError(
|
|
1890
|
+
"x402 resume request does not match the approved merchant.",
|
|
1891
|
+
409,
|
|
1892
|
+
{ status, selectedPayment: option },
|
|
1893
|
+
status.paymentId
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
const optionChainId = chainIdFromNetwork(option.network);
|
|
1897
|
+
if (status.chainId && optionChainId && status.chainId !== optionChainId) {
|
|
1898
|
+
throw new HavenApiError(
|
|
1899
|
+
"x402 resume request does not match the approved network.",
|
|
1900
|
+
409,
|
|
1901
|
+
{ status, selectedPayment: option },
|
|
1902
|
+
status.paymentId
|
|
1903
|
+
);
|
|
1904
|
+
}
|
|
1905
|
+
if (status.token && status.token !== "USDC") {
|
|
1906
|
+
throw new HavenApiError(
|
|
1907
|
+
"x402 resume request does not match the approved token.",
|
|
1908
|
+
409,
|
|
1909
|
+
{ status, selectedPayment: option },
|
|
1910
|
+
status.paymentId
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1913
|
+
const approvedAmount = status.amount ? normalizeDecimal(status.amount) : "";
|
|
1914
|
+
const requestedAmount = normalizeDecimal(decimalFromUsdcAtomic(x402AuthorizationAmount(option)));
|
|
1915
|
+
if (approvedAmount && approvedAmount !== requestedAmount) {
|
|
1916
|
+
throw new HavenApiError(
|
|
1917
|
+
"x402 resume request does not match the approved amount.",
|
|
1918
|
+
409,
|
|
1919
|
+
{ status, selectedPayment: option },
|
|
1920
|
+
status.paymentId
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
var X402FundingLeg = class {
|
|
1925
|
+
delegateKey;
|
|
1926
|
+
delegateAddress;
|
|
1927
|
+
x402Wallet;
|
|
1928
|
+
chainRpcs;
|
|
1929
|
+
post;
|
|
1930
|
+
signForData;
|
|
1931
|
+
assertSignableAuthorizationState;
|
|
1932
|
+
/**
|
|
1933
|
+
* Receipts keyed by idempotency key, held only as long as the underlying
|
|
1934
|
+
* EIP-3009 authorization is valid. The cache belongs to this module rather
|
|
1935
|
+
* than to the facade because its expiry is read out of the authorization
|
|
1936
|
+
* header itself — a 3009 artifact.
|
|
1937
|
+
*/
|
|
1938
|
+
receiptCache = /* @__PURE__ */ new Map();
|
|
1939
|
+
constructor(options) {
|
|
1940
|
+
this.delegateKey = options.delegateKey;
|
|
1941
|
+
this.delegateAddress = options.delegateAddress;
|
|
1942
|
+
this.x402Wallet = options.x402Wallet;
|
|
1943
|
+
this.chainRpcs = options.chainRpcs;
|
|
1944
|
+
this.post = options.post;
|
|
1945
|
+
this.signForData = options.signForData;
|
|
1946
|
+
this.assertSignableAuthorizationState = options.assertSignableAuthorizationState;
|
|
1947
|
+
}
|
|
1948
|
+
// ── Receipt cache ────────────────────────────────────────────────
|
|
1949
|
+
/** A still-valid cached receipt for this key, or undefined. */
|
|
1950
|
+
cachedReceipt(idempotencyKey) {
|
|
1951
|
+
const cached = this.receiptCache.get(idempotencyKey);
|
|
1952
|
+
if (cached && cached.expiresAt > Date.now()) return cached.receipt;
|
|
1953
|
+
return void 0;
|
|
1954
|
+
}
|
|
1955
|
+
cacheReceipt(idempotencyKey, paymentHeader, receipt) {
|
|
1956
|
+
const expiresAt = getPaymentHeaderValidBefore(paymentHeader);
|
|
1957
|
+
if (expiresAt > Date.now()) {
|
|
1958
|
+
this.receiptCache.set(idempotencyKey, { expiresAt, receipt });
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
// ── Authorization ────────────────────────────────────────────────
|
|
1962
|
+
async authorize(paymentRequired, option, idempotencyKey) {
|
|
1963
|
+
const raw = await this.post("/x402", {
|
|
1964
|
+
url: paymentRequired.resource.url,
|
|
1965
|
+
payTo: this.delegateAddress,
|
|
1966
|
+
merchantPayTo: option.payTo,
|
|
1967
|
+
amount: x402AuthorizationAmount(option),
|
|
1968
|
+
asset: option.asset,
|
|
1969
|
+
network: option.network,
|
|
1970
|
+
description: paymentRequired.resource.description,
|
|
1971
|
+
idempotencyKey,
|
|
1972
|
+
// #1360: same explicit funding-leg declaration as createX402Intent —
|
|
1973
|
+
// this local-key path derives payTo from the key (never stale), but the
|
|
1974
|
+
// declaration keeps both writers of the 3009 shape loud-by-default.
|
|
1975
|
+
settlementScheme: "eip3009"
|
|
1976
|
+
});
|
|
1977
|
+
const state = paymentStateFromRaw("x402 payment", raw);
|
|
1978
|
+
const executedReplay = raw.success && raw.tx_hash ? "idempotency-collision" : state?.nextAction === AgentPaymentNextAction.RetryOriginalX402Request ? "approval-resume" : null;
|
|
1979
|
+
if (executedReplay) {
|
|
1980
|
+
const canFund = await this.delegateCanFund(
|
|
1981
|
+
raw.chain_id ?? state?.chainId ?? chainIdFromNetwork(option.network),
|
|
1982
|
+
option.asset,
|
|
1983
|
+
x402AuthorizationAmount(option)
|
|
1984
|
+
);
|
|
1985
|
+
const refuse = executedReplay === "idempotency-collision" ? canFund !== true : canFund === false;
|
|
1986
|
+
if (refuse) {
|
|
1987
|
+
const settledReceipt = state && executedReplay === "approval-resume" ? this.receiptFromStatus(paymentRequired, option, void 0, state) : this.receiptFromAuthorization(paymentRequired, option, void 0, raw);
|
|
1988
|
+
throw new X402AlreadySettledError(
|
|
1989
|
+
canFund === false ? "This x402 payment already settled \u2014 the delegate no longer holds the funds to authorize it again. To buy the same item a second time, pass a distinct `idempotencyKey`; the synthesised key intentionally collapses repeat calls for the same product within a 5-minute window so a retried request cannot pay twice." : "This x402 payment already settled, and whether the delegate can still fund a new authorization could not be verified (no `chainRpcs` entry for this chain). Refusing rather than issue an authorization that may be unfundable. To buy the same item a second time, pass a distinct `idempotencyKey`; to finish an interrupted payment, resume it by `paymentId`.",
|
|
1990
|
+
settledReceipt,
|
|
1991
|
+
canFund === false ? "settled" : "unverifiable"
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
const paymentHeader = await this.createPaymentHeader(paymentRequired, option);
|
|
1996
|
+
if (raw.success && raw.tx_hash) {
|
|
1997
|
+
const receipt2 = this.receiptFromAuthorization(paymentRequired, option, paymentHeader, raw);
|
|
1998
|
+
this.cacheReceipt(idempotencyKey, paymentHeader, receipt2);
|
|
1999
|
+
return receipt2;
|
|
2000
|
+
}
|
|
2001
|
+
if (state?.nextAction === AgentPaymentNextAction.RetryOriginalX402Request) {
|
|
2002
|
+
const receipt2 = this.receiptFromStatus(paymentRequired, option, paymentHeader, state);
|
|
2003
|
+
this.cacheReceipt(idempotencyKey, paymentHeader, receipt2);
|
|
2004
|
+
return receipt2;
|
|
2005
|
+
}
|
|
2006
|
+
this.assertSignableAuthorizationState("x402 payment", raw);
|
|
2007
|
+
if (!raw.sign_data?.hash) {
|
|
2008
|
+
throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
|
|
2009
|
+
}
|
|
2010
|
+
const sig = await this.signForData(raw.sign_data);
|
|
2011
|
+
const execResult = await this.post(
|
|
2012
|
+
`/payments/${raw.payment_id}/sign`,
|
|
2013
|
+
{ signature: sig }
|
|
2014
|
+
);
|
|
2015
|
+
if (execResult.status !== "confirmed") {
|
|
2016
|
+
throwPaymentStateError("x402 payment", execResult);
|
|
2017
|
+
}
|
|
2018
|
+
await this.waitForFundingTx(
|
|
2019
|
+
execResult.tx_hash,
|
|
2020
|
+
execResult.chain_id ?? chainIdFromNetwork(option.network)
|
|
2021
|
+
);
|
|
2022
|
+
const receipt = this.receiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult);
|
|
2023
|
+
this.cacheReceipt(idempotencyKey, paymentHeader, receipt);
|
|
2024
|
+
return receipt;
|
|
2025
|
+
}
|
|
2026
|
+
// ── Header minting ───────────────────────────────────────────────
|
|
2027
|
+
async createPaymentHeader(paymentRequired, option) {
|
|
2028
|
+
if (!this.delegateKey) {
|
|
2029
|
+
throw new HavenSigningError("delegateKey is required to sign x402 payment headers.");
|
|
2030
|
+
}
|
|
2031
|
+
const account = privateKeyToAccount(this.delegateKey);
|
|
2032
|
+
const requirements = toStandardPaymentRequirements(paymentRequired, option);
|
|
2033
|
+
const header = await exact.evm.createPaymentHeader(
|
|
2034
|
+
account,
|
|
2035
|
+
paymentRequired.x402Version,
|
|
2036
|
+
requirements
|
|
2037
|
+
);
|
|
2038
|
+
if (paymentRequired.x402Version < 2) return header;
|
|
2039
|
+
const payment = decodeBase64Json(header);
|
|
2040
|
+
return encodeBase64Json({
|
|
2041
|
+
x402Version: paymentRequired.x402Version,
|
|
2042
|
+
accepted: option,
|
|
2043
|
+
payload: payment.payload
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
// ── Receipt mapping ──────────────────────────────────────────────
|
|
2047
|
+
receiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult) {
|
|
2048
|
+
const txHash = execResult?.tx_hash ?? raw.tx_hash ?? "";
|
|
2049
|
+
const chainId = execResult?.chain_id ?? raw.chain_id ?? chainIdFromNetwork(option.network);
|
|
2050
|
+
const token = execResult?.token ?? raw.token ?? "USDC";
|
|
2051
|
+
const amount = execResult?.amount ?? raw.amount ?? decimalFromUsdcAtomic(x402AuthorizationAmount(option));
|
|
2052
|
+
const to = execResult?.to ?? raw.to ?? this.delegateAddress ?? "";
|
|
2053
|
+
const explorerUrl = execResult?.explorer_url ?? raw.explorer_url ?? explorerUrlOrEmpty(chainId, txHash);
|
|
2054
|
+
const merchantTo = execResult?.merchant_to ?? raw.merchant_to ?? option.payTo;
|
|
2055
|
+
const payer = raw.payer ?? raw.safe_address ?? raw.sign_data?.components.safe;
|
|
2056
|
+
return buildX402Receipt({
|
|
2057
|
+
paymentId: raw.payment_id,
|
|
2058
|
+
txHash,
|
|
2059
|
+
token,
|
|
2060
|
+
amount,
|
|
2061
|
+
to,
|
|
2062
|
+
resourceUrl: paymentRequired.resource.url,
|
|
2063
|
+
explorerUrl,
|
|
2064
|
+
accepted: option,
|
|
2065
|
+
paymentHeader,
|
|
2066
|
+
merchantTo,
|
|
2067
|
+
payer,
|
|
2068
|
+
chainId
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
receiptFromStatus(paymentRequired, option, paymentHeader, status) {
|
|
2072
|
+
if (!status.txHash) {
|
|
2073
|
+
throw new HavenApiError(
|
|
2074
|
+
`x402 payment ${status.paymentId} is ready to retry but has no Haven transaction hash.`,
|
|
2075
|
+
502,
|
|
2076
|
+
status,
|
|
2077
|
+
status.paymentId
|
|
2078
|
+
);
|
|
2079
|
+
}
|
|
2080
|
+
return buildX402Receipt({
|
|
2081
|
+
paymentId: status.paymentId,
|
|
2082
|
+
txHash: status.txHash,
|
|
2083
|
+
token: status.token || "USDC",
|
|
2084
|
+
amount: status.amount || decimalFromUsdcAtomic(x402AuthorizationAmount(option)),
|
|
2085
|
+
to: this.delegateAddress ?? "",
|
|
2086
|
+
resourceUrl: paymentRequired.resource.url,
|
|
2087
|
+
explorerUrl: explorerUrlOrEmpty(status.chainId, status.txHash),
|
|
2088
|
+
accepted: option,
|
|
2089
|
+
paymentHeader,
|
|
2090
|
+
merchantTo: status.merchantAddress ?? option.payTo,
|
|
2091
|
+
payer: this.x402Wallet,
|
|
2092
|
+
chainId: status.chainId || chainIdFromNetwork(option.network)
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
// ── On-chain reads ───────────────────────────────────────────────
|
|
2096
|
+
/**
|
|
2097
|
+
* Wait for a funding tx to be mined with ≥1 confirmation before the
|
|
2098
|
+
* merchant retry, eliminating the race where the merchant's
|
|
2099
|
+
* `balanceOf(delegate)` runs before the funding block propagates.
|
|
2100
|
+
*
|
|
2101
|
+
* Skipped when `chainRpcs` does not include the chain; in that case Haven's
|
|
2102
|
+
* backend has already confirmed on-chain submission and callers accept the
|
|
2103
|
+
* small propagation window as a trade-off for not configuring an RPC URL.
|
|
2104
|
+
*/
|
|
2105
|
+
async waitForFundingTx(txHash, chainId, timeoutMs = 3e4) {
|
|
2106
|
+
if (!txHash || !chainId) return;
|
|
2107
|
+
const rpcUrl = this.chainRpcs[chainId];
|
|
2108
|
+
if (!rpcUrl) return;
|
|
2109
|
+
const provider = createJsonRpcProvider(rpcUrl);
|
|
2110
|
+
const onChainReceipt = await provider.waitForTransaction(txHash, 1, timeoutMs);
|
|
2111
|
+
if (!onChainReceipt || onChainReceipt.status !== 1) {
|
|
2112
|
+
throw new HavenApiError(
|
|
2113
|
+
"Funding tx did not confirm on-chain within the timeout window.",
|
|
2114
|
+
500,
|
|
2115
|
+
{ txHash, chainId }
|
|
2116
|
+
);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2120
|
+
* Can the delegate EOA still fund an authorization for `amountAtomic`?
|
|
2121
|
+
*
|
|
2122
|
+
* #1521: the only question that separates a legitimate resume (funding
|
|
2123
|
+
* confirmed, merchant never paid — the delegate still holds the money) from
|
|
2124
|
+
* a replayed settled payment (funding confirmed, merchant paid, delegate
|
|
2125
|
+
* spent). The intent's own `status: 'confirmed'` is identical in both.
|
|
2126
|
+
*
|
|
2127
|
+
* The balance is asked of the CHAIN rather than of Haven's bookkeeping on
|
|
2128
|
+
* purpose: the merchant-settlement evidence record is written by this SDK
|
|
2129
|
+
* *after* the merchant call, so a client that dies between the two leaves
|
|
2130
|
+
* the backend believing the merchant was never paid — the exact case the
|
|
2131
|
+
* discriminator has to get right. The chain cannot be behind in that way.
|
|
2132
|
+
*
|
|
2133
|
+
* Returns `null` — never a guess — when `chainRpcs` has no entry for the
|
|
2134
|
+
* chain or the read fails. Callers must treat that as "unverifiable", not
|
|
2135
|
+
* as "funded".
|
|
2136
|
+
*/
|
|
2137
|
+
async delegateCanFund(chainId, tokenAddress, amountAtomic, timeoutMs = 1e4) {
|
|
2138
|
+
if (!chainId || !this.delegateAddress) return null;
|
|
2139
|
+
const rpcUrl = this.chainRpcs[chainId];
|
|
2140
|
+
if (!rpcUrl) return null;
|
|
2141
|
+
try {
|
|
2142
|
+
const provider = createJsonRpcProvider(rpcUrl);
|
|
2143
|
+
const token = createErc20Contract(
|
|
2144
|
+
tokenAddress,
|
|
2145
|
+
["function balanceOf(address) view returns (uint256)"],
|
|
2146
|
+
provider
|
|
2147
|
+
);
|
|
2148
|
+
const balance = await Promise.race([
|
|
2149
|
+
token.balanceOf(this.delegateAddress),
|
|
2150
|
+
new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs).unref?.())
|
|
2151
|
+
]);
|
|
2152
|
+
if (balance === null) return null;
|
|
2153
|
+
return balance >= BigInt(amountAtomic);
|
|
2154
|
+
} catch {
|
|
2155
|
+
return null;
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
};
|
|
2159
|
+
function getPaymentHeaderValidBefore(paymentHeader) {
|
|
2160
|
+
try {
|
|
2161
|
+
const payment = decodeBase64Json(
|
|
2162
|
+
paymentHeader
|
|
2163
|
+
);
|
|
2164
|
+
const payload = payment.payload;
|
|
2165
|
+
const validBeforeSeconds = Number(payload.authorization?.validBefore);
|
|
2166
|
+
if (Number.isFinite(validBeforeSeconds)) return validBeforeSeconds * 1e3;
|
|
2167
|
+
} catch {
|
|
2168
|
+
}
|
|
2169
|
+
return 0;
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
// src/x402-erc7710.ts
|
|
2173
|
+
var X402Erc7710 = class {
|
|
2174
|
+
delegateKey;
|
|
2175
|
+
post;
|
|
2176
|
+
signForData;
|
|
2177
|
+
getAgent;
|
|
2178
|
+
constructor(options) {
|
|
2179
|
+
this.delegateKey = options.delegateKey;
|
|
2180
|
+
this.post = options.post;
|
|
2181
|
+
this.signForData = options.signForData;
|
|
2182
|
+
this.getAgent = options.getAgent;
|
|
2183
|
+
}
|
|
2184
|
+
/**
|
|
2185
|
+
* Pay a merchant through **erc7710 direct settlement** (#1454, epic #1450).
|
|
2186
|
+
*
|
|
2187
|
+
* The whole point of this path is what it does NOT do. There is no funding
|
|
2188
|
+
* leg: the merchant redeems a delegation chain and pulls from the treasury
|
|
2189
|
+
* directly, so the delegate EOA never holds the money, no sweep can strand
|
|
2190
|
+
* it, and the #713 reconciliation class does not apply. It is also why this
|
|
2191
|
+
* method is SMALLER than the 3009 path — the backend assembles the merchant
|
|
2192
|
+
* `X-PAYMENT` header in `assembleSettlementPayload`, so the SDK builds no
|
|
2193
|
+
* header locally.
|
|
2194
|
+
*
|
|
2195
|
+
* authorize (payTo = the MERCHANT) → sign the child → settle → header
|
|
2196
|
+
*
|
|
2197
|
+
* The caller then retries the merchant with that header. **Nothing has
|
|
2198
|
+
* settled when this returns** — that is why it does not return an
|
|
2199
|
+
* `X402Receipt`.
|
|
2200
|
+
*
|
|
2201
|
+
* Requires a delegation-rail account. The backend enforces that
|
|
2202
|
+
* (`validateGenericSchemeRail`), and so does this method, before building a
|
|
2203
|
+
* request the backend would only reject: an error a client can explain is
|
|
2204
|
+
* worth more than a 400 it has to decode.
|
|
2205
|
+
*
|
|
2206
|
+
* **MCP callers must pass `options.resourceUrl`.** An in-band MCP 402
|
|
2207
|
+
* challenge frequently carries no `resource` object at all, so
|
|
2208
|
+
* `paymentRequired.resource?.url` is undefined and the backend answers
|
|
2209
|
+
* "Valid url is required". The QA scenario this path was ported from falls
|
|
2210
|
+
* back to the request URL for exactly that reason — the SDK cannot, because
|
|
2211
|
+
* it never saw the request. Pass it.
|
|
2212
|
+
*/
|
|
2213
|
+
async settle(paymentRequired, options = {}) {
|
|
2214
|
+
if (!this.delegateKey) {
|
|
2215
|
+
throw new HavenSigningError(
|
|
2216
|
+
"delegateKey is required for x402 payments. Pass it in the HavenClient config."
|
|
2217
|
+
);
|
|
2218
|
+
}
|
|
2219
|
+
const prepared = await this.prepare(paymentRequired, options);
|
|
2220
|
+
const signature = await this.signForData(prepared.signData);
|
|
2221
|
+
const paymentHeader = await this.submit(prepared.paymentId, signature);
|
|
2222
|
+
return { ...prepared.settlement, paymentHeader };
|
|
2223
|
+
}
|
|
2224
|
+
/**
|
|
2225
|
+
* The AUTHORIZE half of erc7710 settlement (#1456): select the scheme, build
|
|
2226
|
+
* the request, and return the child to be signed — without signing it.
|
|
2227
|
+
*
|
|
2228
|
+
* Split out because the hosted topology cannot use `settleX402Erc7710()`:
|
|
2229
|
+
* that method signs in-process with `delegateKey`, and hosted Haven does not
|
|
2230
|
+
* have one and must not. The hosted MCP server drives these two halves with
|
|
2231
|
+
* the LOCAL signer in between, so the key stays where it belongs and the
|
|
2232
|
+
* request shaping stays in one place rather than being reimplemented.
|
|
2233
|
+
*/
|
|
2234
|
+
async prepare(paymentRequired, options = {}) {
|
|
2235
|
+
const delegationRail = options.delegationRail ?? (await this.getAgent()).executionRail === "delegation";
|
|
2236
|
+
if (!delegationRail) {
|
|
2237
|
+
throw new HavenApiError(
|
|
2238
|
+
"erc7710 settlement requires a delegation-rail account; this one is not on it. Use authorizeX402() for the standard EIP-3009 path.",
|
|
2239
|
+
400
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2242
|
+
const selection = selectX402SettlementScheme(paymentRequired.accepts, { delegationRail });
|
|
2243
|
+
if (!selection || selection.scheme !== "erc7710") {
|
|
2244
|
+
throw new HavenApiError(
|
|
2245
|
+
"This merchant does not advertise an erc7710 settlement option (no accepts[] entry carries extra.assetTransferMethod: 'erc7710'). Use authorizeX402() for the standard EIP-3009 path.",
|
|
2246
|
+
400
|
|
2247
|
+
);
|
|
2248
|
+
}
|
|
2249
|
+
const option = selection.option;
|
|
2250
|
+
const merchantPayTo = option.payTo;
|
|
2251
|
+
const amountAtomic = x402AuthorizationAmount(option);
|
|
2252
|
+
const raw = await this.post("/x402", {
|
|
2253
|
+
url: options.resourceUrl ?? paymentRequired.resource?.url,
|
|
2254
|
+
// payTo = the MERCHANT is what selects direct settlement server-side.
|
|
2255
|
+
// The explicit settlementScheme must AGREE with that shape (#1360) —
|
|
2256
|
+
// disagreement is a 400 by design, so that a stale delegate address
|
|
2257
|
+
// becomes a loud mismatch instead of a silent reroute to the 3009 leg.
|
|
2258
|
+
payTo: merchantPayTo,
|
|
2259
|
+
settlementScheme: "erc7710",
|
|
2260
|
+
amount: amountAtomic,
|
|
2261
|
+
asset: option.asset,
|
|
2262
|
+
network: option.network,
|
|
2263
|
+
// The v2 header echoes the accepted entry field-for-field, so the quoted
|
|
2264
|
+
// timeout must round-trip or the merchant rejects the echo (#1064).
|
|
2265
|
+
maxTimeoutSeconds: option.maxTimeoutSeconds,
|
|
2266
|
+
// #1058: forward the advertised facilitators verbatim — the child becomes
|
|
2267
|
+
// redeemable ONLY by them. `null` here means the merchant advertised none
|
|
2268
|
+
// (or an empty array, which the backend 400s on), so the field is OMITTED
|
|
2269
|
+
// rather than sent empty. See x402FacilitatorAddresses.
|
|
2270
|
+
...selection.facilitatorAddresses ? { facilitatorAddresses: selection.facilitatorAddresses } : {},
|
|
2271
|
+
// #1307/#1547: persisted so the settle leg can rehydrate the merchant
|
|
2272
|
+
// call by payment_id on this scheme too, not only on the 3009 bridge.
|
|
2273
|
+
...options.mcpCallContext ? { mcpCallContext: options.mcpCallContext } : {}
|
|
2274
|
+
});
|
|
2275
|
+
if (!raw.payment_id) {
|
|
2276
|
+
throw new HavenApiError("No payment_id returned from x402/authorize", 500, raw);
|
|
2277
|
+
}
|
|
2278
|
+
const signData = raw.sign_data;
|
|
2279
|
+
if (signData?.signature_scheme !== "eip712_delegation" || !signData.typed_data) {
|
|
2280
|
+
throw new HavenApiError(
|
|
2281
|
+
`x402/authorize did not return an erc7710 settlement child (signature_scheme was ${JSON.stringify(signData?.signature_scheme)}). Refusing to sign a payload this path did not ask for.`,
|
|
2282
|
+
500,
|
|
2283
|
+
raw
|
|
2284
|
+
);
|
|
2285
|
+
}
|
|
2286
|
+
return {
|
|
2287
|
+
paymentId: raw.payment_id,
|
|
2288
|
+
signData,
|
|
2289
|
+
settlement: {
|
|
2290
|
+
paymentId: raw.payment_id,
|
|
2291
|
+
merchantPayTo,
|
|
2292
|
+
amountAtomic,
|
|
2293
|
+
asset: option.asset,
|
|
2294
|
+
network: option.network,
|
|
2295
|
+
facilitatorAddresses: selection.facilitatorAddresses
|
|
2296
|
+
}
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
/**
|
|
2300
|
+
* The SETTLE half (#1456): exchange the signed child for the merchant header.
|
|
2301
|
+
*
|
|
2302
|
+
* The SDK builds no header on this path — the backend assembles the MetaMask
|
|
2303
|
+
* erc7710 payload in `assembleSettlementPayload`. Whoever produced the
|
|
2304
|
+
* signature (an in-process delegate key, or the local edge signer over the
|
|
2305
|
+
* hosted boundary) is irrelevant here.
|
|
2306
|
+
*/
|
|
2307
|
+
async submit(paymentId, signature) {
|
|
2308
|
+
const settled = await this.post(
|
|
2309
|
+
`/x402/${paymentId}/settle`,
|
|
2310
|
+
{ signature }
|
|
2311
|
+
);
|
|
2312
|
+
if (!settled.payment_header) {
|
|
2313
|
+
throw new HavenApiError(
|
|
2314
|
+
"x402 settle returned no payment_header \u2014 the merchant cannot be retried.",
|
|
2315
|
+
500,
|
|
2316
|
+
settled
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
return settled.payment_header;
|
|
2320
|
+
}
|
|
2321
|
+
};
|
|
2322
|
+
|
|
2323
|
+
// src/tool-adapter.ts
|
|
2324
|
+
function toolX402PaymentRequired(input) {
|
|
2325
|
+
return {
|
|
2326
|
+
x402Version: 2,
|
|
2327
|
+
resource: { url: input.url, description: input.description },
|
|
2328
|
+
accepts: [
|
|
2329
|
+
{
|
|
2330
|
+
scheme: "exact",
|
|
2331
|
+
network: input.network,
|
|
2332
|
+
amount: input.amount,
|
|
2333
|
+
asset: input.asset,
|
|
2334
|
+
payTo: input.payTo,
|
|
2335
|
+
maxTimeoutSeconds: 30
|
|
2336
|
+
}
|
|
2337
|
+
]
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
function x402ToolReceipt(receipt) {
|
|
2341
|
+
return {
|
|
2342
|
+
success: true,
|
|
2343
|
+
payment_id: receipt.paymentId,
|
|
2344
|
+
tx_hash: receipt.txHash,
|
|
2345
|
+
token: receipt.token,
|
|
2346
|
+
amount: receipt.amount,
|
|
2347
|
+
to: receipt.to,
|
|
2348
|
+
resource_url: receipt.resourceUrl,
|
|
2349
|
+
explorer_url: receipt.explorerUrl,
|
|
2350
|
+
payment_header: receipt.paymentHeader,
|
|
2351
|
+
merchant_to: receipt.merchantTo,
|
|
2352
|
+
payer: receipt.payer,
|
|
2353
|
+
chain_id: receipt.chainId,
|
|
2354
|
+
haven: receipt.haven,
|
|
2355
|
+
merchant: receipt.merchant,
|
|
2356
|
+
x402: receipt.x402
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
function toolError(err) {
|
|
2360
|
+
if (err instanceof HavenPaymentStateError) {
|
|
2361
|
+
return {
|
|
2362
|
+
success: false,
|
|
2363
|
+
payment_id: err.state.paymentId,
|
|
2364
|
+
kind: err.state.kind,
|
|
2365
|
+
rail: err.state.rail,
|
|
2366
|
+
status: err.state.status,
|
|
2367
|
+
phase: err.state.phase,
|
|
2368
|
+
next_action: err.state.nextAction,
|
|
2369
|
+
tx_hash: err.state.txHash,
|
|
2370
|
+
token: err.state.token,
|
|
2371
|
+
amount: err.state.amount,
|
|
2372
|
+
resource_url: err.state.resourceUrl,
|
|
2373
|
+
merchant_address: err.state.merchantAddress,
|
|
2374
|
+
amount_atomic: err.state.amountAtomic,
|
|
2375
|
+
asset: err.state.asset,
|
|
2376
|
+
network: err.state.network,
|
|
2377
|
+
description: err.state.description,
|
|
2378
|
+
idempotency_key: err.state.idempotencyKey,
|
|
2379
|
+
x402: err.state.x402 ? {
|
|
2380
|
+
amount_atomic: err.state.x402.amountAtomic,
|
|
2381
|
+
asset: err.state.x402.asset,
|
|
2382
|
+
network: err.state.x402.network,
|
|
2383
|
+
resource_url: err.state.x402.resourceUrl,
|
|
2384
|
+
merchant_address: err.state.x402.merchantAddress,
|
|
2385
|
+
description: err.state.x402.description,
|
|
2386
|
+
idempotency_key: err.state.x402.idempotencyKey
|
|
2387
|
+
} : void 0,
|
|
2388
|
+
mpp: err.state.mpp ? {
|
|
2389
|
+
amount_atomic: err.state.mpp.amountAtomic,
|
|
2390
|
+
asset: err.state.mpp.asset,
|
|
2391
|
+
network: err.state.mpp.network,
|
|
2392
|
+
resource_url: err.state.mpp.resourceUrl,
|
|
2393
|
+
merchant_address: err.state.mpp.merchantAddress,
|
|
2394
|
+
description: err.state.mpp.description,
|
|
2395
|
+
idempotency_key: err.state.mpp.idempotencyKey,
|
|
2396
|
+
challenge_id: err.state.mpp.challengeId
|
|
2397
|
+
} : void 0,
|
|
2398
|
+
resume_state: err.resumeState,
|
|
2399
|
+
expires_at: err.state.expiresAt,
|
|
2400
|
+
chain_id: err.state.chainId,
|
|
2401
|
+
message: err.state.message,
|
|
2402
|
+
error: err.message
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
if (err instanceof HavenApiError) {
|
|
2406
|
+
return {
|
|
2407
|
+
success: false,
|
|
2408
|
+
status_code: err.statusCode,
|
|
2409
|
+
error: err.message,
|
|
2410
|
+
body: err.body
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
return {
|
|
2414
|
+
success: false,
|
|
2415
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
// src/merchant-completion.ts
|
|
2420
|
+
var MERCHANT_BODY_SNIPPET_LIMIT = 1e3;
|
|
2421
|
+
var MerchantCompletion = class {
|
|
2422
|
+
post;
|
|
2423
|
+
merchantTransport;
|
|
2424
|
+
getPaymentStatus;
|
|
2425
|
+
getAgent;
|
|
2426
|
+
delegateAddress;
|
|
2427
|
+
x402Wallet;
|
|
2428
|
+
constructor(options) {
|
|
2429
|
+
this.post = options.post;
|
|
2430
|
+
this.merchantTransport = options.merchantTransport;
|
|
2431
|
+
this.getPaymentStatus = options.getPaymentStatus;
|
|
2432
|
+
this.getAgent = options.getAgent;
|
|
2433
|
+
this.delegateAddress = options.delegateAddress;
|
|
2434
|
+
this.x402Wallet = options.x402Wallet;
|
|
2435
|
+
}
|
|
2436
|
+
async retryRequest(url, initialInit, paymentRequired, receipt) {
|
|
2437
|
+
if (!receipt.accepted) {
|
|
2438
|
+
throw new HavenApiError("No accepted x402 option was recorded for payment retry", 500);
|
|
2439
|
+
}
|
|
2440
|
+
if (!receipt.paymentHeader) {
|
|
2441
|
+
throw new HavenApiError("No x402 payment header was returned for payment retry", 500);
|
|
2442
|
+
}
|
|
2443
|
+
const retryResponse = await this.merchantTransport.deliverPayment(
|
|
2444
|
+
url,
|
|
2445
|
+
initialInit,
|
|
2446
|
+
receipt.paymentHeader
|
|
2447
|
+
);
|
|
2448
|
+
if (!retryResponse.ok) {
|
|
2449
|
+
const merchant = await captureMerchantResponse(retryResponse);
|
|
2450
|
+
await this.recordRetryRejected({
|
|
2451
|
+
rail: "x402",
|
|
2452
|
+
paymentId: receipt.paymentId,
|
|
2453
|
+
txHash: receipt.txHash,
|
|
2454
|
+
resourceUrl: receipt.resourceUrl,
|
|
2455
|
+
merchant,
|
|
2456
|
+
details: {
|
|
2457
|
+
merchant_to: receipt.merchantTo,
|
|
2458
|
+
delegate_to: receipt.to
|
|
2459
|
+
}
|
|
2460
|
+
});
|
|
2461
|
+
throw new HavenApiError(
|
|
2462
|
+
"x402 retry failed after Haven funded the delegate wallet; reconciliation may be required.",
|
|
2463
|
+
merchant.merchant_status,
|
|
2464
|
+
{
|
|
2465
|
+
marker: "x402_retry_rejected_after_funding",
|
|
2466
|
+
payment_id: receipt.paymentId,
|
|
2467
|
+
tx_hash: receipt.txHash,
|
|
2468
|
+
resource_url: receipt.resourceUrl,
|
|
2469
|
+
merchant_to: receipt.merchantTo,
|
|
2470
|
+
delegate_to: receipt.to,
|
|
2471
|
+
...merchant
|
|
2472
|
+
}
|
|
2473
|
+
);
|
|
2474
|
+
}
|
|
2475
|
+
const merchantSettlement = parseMerchantSettlement(retryResponse.headers.get("PAYMENT-RESPONSE"));
|
|
2476
|
+
if (receipt.merchant && merchantSettlement.settlementTxHash) {
|
|
2477
|
+
receipt.merchant.settlementTxHash = merchantSettlement.settlementTxHash;
|
|
2478
|
+
receipt.merchant.settlementExplorerUrl = buildExplorerUrl(
|
|
2479
|
+
receipt.chainId,
|
|
2480
|
+
merchantSettlement.settlementTxHash
|
|
2481
|
+
);
|
|
2482
|
+
}
|
|
2483
|
+
await this.reportEvidence({
|
|
2484
|
+
paymentId: receipt.paymentId,
|
|
2485
|
+
rail: "x402",
|
|
2486
|
+
txHash: receipt.txHash,
|
|
2487
|
+
resourceUrl: receipt.resourceUrl,
|
|
2488
|
+
merchantStatus: retryResponse.status,
|
|
2489
|
+
challengePayload: paymentRequired,
|
|
2490
|
+
selectedPayment: receipt.accepted,
|
|
2491
|
+
paymentProofHeaderName: "X-PAYMENT",
|
|
2492
|
+
paymentProofHeader: receipt.paymentHeader,
|
|
2493
|
+
protocolReceiptHeaderName: "PAYMENT-RESPONSE",
|
|
2494
|
+
protocolReceiptHeader: retryResponse.headers.get("PAYMENT-RESPONSE") ?? void 0
|
|
2495
|
+
});
|
|
2496
|
+
await this.reportMerchantReceipt(receipt.paymentId, retryResponse);
|
|
2497
|
+
return retryResponse;
|
|
2498
|
+
}
|
|
2499
|
+
/**
|
|
2500
|
+
* #956: capture the merchant's OWN receipt when the paid response carries
|
|
2501
|
+
* one, and report it to Haven so the reporting feed can attach it next to
|
|
2502
|
+
* the Haven-generated payment evidence (#498). Two supported signals on the
|
|
2503
|
+
* paid response:
|
|
2504
|
+
*
|
|
2505
|
+
* x-receipt-json: base64-encoded JSON receipt document (inline)
|
|
2506
|
+
* x-receipt-url: https URL to the receipt document (reference)
|
|
2507
|
+
*
|
|
2508
|
+
* Strictly best-effort: absence is the normal case, and no failure here may
|
|
2509
|
+
* ever affect the completed payment — the response is already paid for.
|
|
2510
|
+
*/
|
|
2511
|
+
async reportMerchantReceipt(paymentId, response) {
|
|
2512
|
+
try {
|
|
2513
|
+
const inlineB64 = response.headers.get("x-receipt-json");
|
|
2514
|
+
const url = response.headers.get("x-receipt-url");
|
|
2515
|
+
if (!inlineB64 && !url) return;
|
|
2516
|
+
let body = null;
|
|
2517
|
+
if (inlineB64) {
|
|
2518
|
+
if (inlineB64.length > Math.ceil(64 * 1024 * 4 / 3)) return;
|
|
2519
|
+
const decoded = JSON.parse(Buffer.from(inlineB64, "base64").toString("utf8"));
|
|
2520
|
+
if (decoded && typeof decoded === "object") body = { json: decoded };
|
|
2521
|
+
} else if (url && url.startsWith("https://") && url.length <= 2048) {
|
|
2522
|
+
body = { url };
|
|
2523
|
+
}
|
|
2524
|
+
if (!body) return;
|
|
2525
|
+
await this.post(`/machine-payments/${paymentId}/merchant-receipt`, body);
|
|
2526
|
+
} catch {
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
async resolveCompletionContext(input) {
|
|
2530
|
+
const status = await this.getPaymentStatus(input.paymentId);
|
|
2531
|
+
if (status.rail !== "x402") {
|
|
2532
|
+
throw new HavenPaymentStateError(
|
|
2533
|
+
`Payment ${status.paymentId} is ${status.rail}, not x402.`,
|
|
2534
|
+
409,
|
|
2535
|
+
status
|
|
2536
|
+
);
|
|
2537
|
+
}
|
|
2538
|
+
const readyForMerchantCompletion = input.noFundingLeg ? status.kind === "payment_intent" && status.status === "submitted" : status.nextAction === AgentPaymentNextAction.RetryOriginalX402Request || status.kind === "payment_intent" && status.status === "confirmed" && status.phase === AgentPaymentPhase.PaymentConfirmed && status.nextAction === AgentPaymentNextAction.None;
|
|
2539
|
+
if (!readyForMerchantCompletion) {
|
|
2540
|
+
throw new HavenPaymentStateError(status.message, paymentStateStatusCode(status.status, 409), status);
|
|
2541
|
+
}
|
|
2542
|
+
if (!input.noFundingLeg && !status.txHash) {
|
|
2543
|
+
throw new HavenApiError(
|
|
2544
|
+
`x402 payment ${status.paymentId} is ready for merchant completion but has no Haven transaction hash.`,
|
|
2545
|
+
502,
|
|
2546
|
+
status,
|
|
2547
|
+
status.paymentId
|
|
2548
|
+
);
|
|
2549
|
+
}
|
|
2550
|
+
const approvedResourceUrl = status.resourceUrl ?? status.x402?.resourceUrl ?? null;
|
|
2551
|
+
if (approvedResourceUrl && approvedResourceUrl !== input.url) {
|
|
2552
|
+
throw new HavenApiError(
|
|
2553
|
+
"x402 merchant completion does not match the approved resource URL.",
|
|
2554
|
+
409,
|
|
2555
|
+
{ status, url: input.url },
|
|
2556
|
+
status.paymentId
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2559
|
+
return {
|
|
2560
|
+
paymentId: status.paymentId,
|
|
2561
|
+
txHash: status.txHash,
|
|
2562
|
+
resourceUrl: approvedResourceUrl ?? input.url,
|
|
2563
|
+
merchantAddress: status.merchantAddress ?? status.x402?.merchantAddress ?? null
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2566
|
+
async resolveWalletForMerchantCall() {
|
|
2567
|
+
const localWallet = x402PayerAddress(this.delegateAddress, this.x402Wallet);
|
|
2568
|
+
if (localWallet) return localWallet;
|
|
2569
|
+
try {
|
|
2570
|
+
const agent = await this.getAgent();
|
|
2571
|
+
return agent.delegateAddress ?? void 0;
|
|
2572
|
+
} catch {
|
|
2573
|
+
return void 0;
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
// #1328: authorizeMachinePayment / authorizeMppDemoPayment / resumeAuthorizedMpp
|
|
2577
|
+
// / resumeMppPayment / fetchWithMachinePayment / retryMppRequest (the
|
|
2578
|
+
// MACHINE-PAYMENT-CHALLENGE / mpp_demo client surface) are retired — the
|
|
2579
|
+
// backend's POST /machine-payments/authorize refuses unconditionally now,
|
|
2580
|
+
// and MACHINE-PAYMENT-CHALLENGE was never produced by any other Haven
|
|
2581
|
+
// surface. Use the x402 flow (authorizeX402 / fetch / quoteX402 / payX402Quote)
|
|
2582
|
+
// for agent-to-merchant payments.
|
|
2583
|
+
async recordRetryRejected(input) {
|
|
2584
|
+
try {
|
|
2585
|
+
await this.post("/machine-payments/reconciliation-events", {
|
|
2586
|
+
paymentId: input.paymentId,
|
|
2587
|
+
rail: input.rail,
|
|
2588
|
+
eventType: "merchant_retry_rejected_after_payment",
|
|
2589
|
+
txHash: input.txHash,
|
|
2590
|
+
reason: `Merchant returned HTTP ${input.merchant.merchant_status} after Haven payment confirmation`,
|
|
2591
|
+
details: {
|
|
2592
|
+
resource_url: input.resourceUrl,
|
|
2593
|
+
retry_status: input.merchant.merchant_status,
|
|
2594
|
+
retry_body: input.merchant.merchant_body.slice(0, MERCHANT_BODY_SNIPPET_LIMIT) || null,
|
|
2595
|
+
...input.details
|
|
2596
|
+
}
|
|
2597
|
+
});
|
|
2598
|
+
} catch {
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
async reportEvidence(input) {
|
|
2602
|
+
try {
|
|
2603
|
+
await this.post("/machine-payments/evidence", {
|
|
2604
|
+
paymentId: input.paymentId,
|
|
2605
|
+
rail: input.rail,
|
|
2606
|
+
txHash: input.txHash,
|
|
2607
|
+
resourceUrl: input.resourceUrl,
|
|
2608
|
+
merchantStatus: input.merchantStatus,
|
|
2609
|
+
challengePayload: input.challengePayload,
|
|
2610
|
+
selectedPayment: input.selectedPayment,
|
|
2611
|
+
paymentProofHeaderName: input.paymentProofHeaderName,
|
|
2612
|
+
paymentProofHeader: input.paymentProofHeader,
|
|
2613
|
+
protocolReceiptHeaderName: input.protocolReceiptHeaderName,
|
|
2614
|
+
protocolReceiptHeader: input.protocolReceiptHeader,
|
|
2615
|
+
protocolReceiptPayload: input.protocolReceiptHeader ? parseProtocolReceiptHeader(input.protocolReceiptHeader) : void 0
|
|
2616
|
+
});
|
|
2617
|
+
} catch {
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
};
|
|
2621
|
+
function parseMerchantSettlement(header) {
|
|
2622
|
+
if (!header) return {};
|
|
2623
|
+
const parsed = parseProtocolReceiptHeader(header);
|
|
2624
|
+
const tx = typeof parsed?.transaction === "string" ? parsed.transaction : typeof parsed?.txHash === "string" ? parsed.txHash : typeof parsed?.tx_hash === "string" ? parsed.tx_hash : null;
|
|
2625
|
+
return { settlementTxHash: tx };
|
|
2626
|
+
}
|
|
2627
|
+
function parseProtocolReceiptHeader(value) {
|
|
2628
|
+
try {
|
|
2629
|
+
return decodeBase64Json(value);
|
|
2630
|
+
} catch {
|
|
2631
|
+
try {
|
|
2632
|
+
return JSON.parse(value);
|
|
2633
|
+
} catch {
|
|
2634
|
+
return void 0;
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
// src/client.ts
|
|
2640
|
+
var DEFAULT_CONFIRMATION_TIMEOUT = 9e4;
|
|
2641
|
+
var DEFAULT_POLLING_INTERVAL = 3e3;
|
|
1140
2642
|
function x402TypedDataDigest(typedData) {
|
|
1141
2643
|
if (!typedData || typeof typedData !== "object") return void 0;
|
|
1142
2644
|
try {
|
|
@@ -1167,48 +2669,78 @@ function mapCatalogEntry(entry) {
|
|
|
1167
2669
|
};
|
|
1168
2670
|
}
|
|
1169
2671
|
var HavenClient = class {
|
|
1170
|
-
apiKey;
|
|
1171
2672
|
delegateKey;
|
|
1172
|
-
|
|
2673
|
+
havenApi;
|
|
2674
|
+
accountReads;
|
|
2675
|
+
delegateSweep;
|
|
1173
2676
|
x402Wallet;
|
|
1174
|
-
|
|
1175
|
-
merchantTimeout;
|
|
2677
|
+
merchantTransport;
|
|
1176
2678
|
confirmationTimeout;
|
|
1177
2679
|
pollingInterval;
|
|
1178
2680
|
chainRpcs;
|
|
1179
2681
|
inFlightX402 = /* @__PURE__ */ new Map();
|
|
1180
|
-
x402ReceiptCache = /* @__PURE__ */ new Map();
|
|
1181
2682
|
/**
|
|
1182
|
-
*
|
|
1183
|
-
*
|
|
1184
|
-
* scoping so concurrent requests don't race on shared mutable state.
|
|
2683
|
+
* The EIP-3009 funding-leg lifecycle (#1618). The facade holds a reference
|
|
2684
|
+
* and delegates; it does not reimplement any of it.
|
|
1185
2685
|
*/
|
|
1186
|
-
|
|
2686
|
+
fundingLeg;
|
|
1187
2687
|
/**
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1190
|
-
* overlapping async work — like two MCP tool dispatches in flight at
|
|
1191
|
-
* the same time — see their own headers without stepping on each other.
|
|
2688
|
+
* The erc7710 direct-settlement lifecycle (#1619). Separate from the funding
|
|
2689
|
+
* leg on purpose: this scheme has no funding leg to share.
|
|
1192
2690
|
*/
|
|
1193
|
-
|
|
1194
|
-
/**
|
|
1195
|
-
|
|
2691
|
+
erc7710;
|
|
2692
|
+
/**
|
|
2693
|
+
* Merchant delivery and the evidence trail behind it (#1620). Scheme-neutral
|
|
2694
|
+
* on purpose — both settlement schemes finish through the same door.
|
|
2695
|
+
*/
|
|
2696
|
+
merchantCompletion;
|
|
1196
2697
|
/** Delegate address derived from the private key (if provided) */
|
|
1197
2698
|
delegateAddress;
|
|
1198
2699
|
constructor(config) {
|
|
1199
|
-
this.apiKey = config.apiKey;
|
|
1200
2700
|
this.delegateKey = config.delegateKey;
|
|
1201
|
-
this.
|
|
2701
|
+
this.havenApi = new HavenApiTransport(config);
|
|
2702
|
+
this.accountReads = new AccountReads({
|
|
2703
|
+
transport: this.havenApi,
|
|
2704
|
+
getPaymentStatus: (paymentId) => this.getPaymentStatus(paymentId)
|
|
2705
|
+
});
|
|
2706
|
+
this.delegateSweep = new DelegateSweepApi({
|
|
2707
|
+
transport: this.havenApi,
|
|
2708
|
+
delegateKey: config.delegateKey,
|
|
2709
|
+
chainRpcs: config.chainRpcs ?? {},
|
|
2710
|
+
getAgent: () => this.getAgent(),
|
|
2711
|
+
buildExplorerUrl: (chainId, hash) => buildExplorerUrl(chainId, hash)
|
|
2712
|
+
});
|
|
1202
2713
|
this.x402Wallet = config.x402Wallet;
|
|
1203
|
-
this.
|
|
1204
|
-
this.merchantTimeout = config.merchantTimeout ?? DEFAULT_MERCHANT_TIMEOUT;
|
|
2714
|
+
this.merchantTransport = new McpMerchantTransport({ merchantTimeout: config.merchantTimeout });
|
|
1205
2715
|
this.confirmationTimeout = config.confirmationTimeout ?? DEFAULT_CONFIRMATION_TIMEOUT;
|
|
1206
2716
|
this.pollingInterval = config.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
|
|
1207
2717
|
this.chainRpcs = config.chainRpcs ?? {};
|
|
1208
|
-
this.defaultHeaders = { ...config.defaultHeaders ?? {} };
|
|
1209
2718
|
if (this.delegateKey) {
|
|
1210
2719
|
this.delegateAddress = addressFromKey(this.delegateKey);
|
|
1211
2720
|
}
|
|
2721
|
+
this.fundingLeg = new X402FundingLeg({
|
|
2722
|
+
delegateKey: this.delegateKey,
|
|
2723
|
+
delegateAddress: this.delegateAddress,
|
|
2724
|
+
x402Wallet: this.x402Wallet,
|
|
2725
|
+
chainRpcs: this.chainRpcs,
|
|
2726
|
+
post: (path, body) => this.post(path, body),
|
|
2727
|
+
signForData: (signData) => this.signForData(signData),
|
|
2728
|
+
assertSignableAuthorizationState: (label, raw) => this.throwIfNonSignableAuthorizationState(label, raw)
|
|
2729
|
+
});
|
|
2730
|
+
this.merchantCompletion = new MerchantCompletion({
|
|
2731
|
+
post: (path, body) => this.post(path, body),
|
|
2732
|
+
merchantTransport: this.merchantTransport,
|
|
2733
|
+
getPaymentStatus: (paymentId) => this.getPaymentStatus(paymentId),
|
|
2734
|
+
getAgent: () => this.getAgent(),
|
|
2735
|
+
delegateAddress: this.delegateAddress,
|
|
2736
|
+
x402Wallet: this.x402Wallet
|
|
2737
|
+
});
|
|
2738
|
+
this.erc7710 = new X402Erc7710({
|
|
2739
|
+
delegateKey: this.delegateKey,
|
|
2740
|
+
post: (path, body) => this.post(path, body),
|
|
2741
|
+
signForData: (signData) => this.signForData(signData),
|
|
2742
|
+
getAgent: () => this.getAgent()
|
|
2743
|
+
});
|
|
1212
2744
|
}
|
|
1213
2745
|
/**
|
|
1214
2746
|
* Run `fn` with extra Haven-API headers scoped to the async work it
|
|
@@ -1226,7 +2758,7 @@ var HavenClient = class {
|
|
|
1226
2758
|
* context.
|
|
1227
2759
|
*/
|
|
1228
2760
|
withRequestContext(headers, fn) {
|
|
1229
|
-
return this.
|
|
2761
|
+
return this.havenApi.withRequestContext(headers, fn);
|
|
1230
2762
|
}
|
|
1231
2763
|
// ── High-Level API ───────────────────────────────────────────────
|
|
1232
2764
|
/**
|
|
@@ -1262,7 +2794,7 @@ var HavenClient = class {
|
|
|
1262
2794
|
...request.idempotencyKey ? { idempotency_key: request.idempotencyKey } : {}
|
|
1263
2795
|
});
|
|
1264
2796
|
if (raw.status === "pending_approval") {
|
|
1265
|
-
|
|
2797
|
+
throwPaymentStateError("Payment", raw);
|
|
1266
2798
|
}
|
|
1267
2799
|
return {
|
|
1268
2800
|
paymentId: raw.payment_id,
|
|
@@ -1325,7 +2857,7 @@ var HavenClient = class {
|
|
|
1325
2857
|
...new TextEncoder().encode(JSON.stringify(paymentRequired)).length <= 65536 ? { paymentRequired } : {}
|
|
1326
2858
|
});
|
|
1327
2859
|
if (raw.status !== "pending_signature") {
|
|
1328
|
-
|
|
2860
|
+
throwPaymentStateError("x402 payment", raw);
|
|
1329
2861
|
}
|
|
1330
2862
|
if (!raw.sign_data?.hash) {
|
|
1331
2863
|
throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
|
|
@@ -1446,7 +2978,7 @@ var HavenClient = class {
|
|
|
1446
2978
|
*/
|
|
1447
2979
|
async getPayment(paymentId) {
|
|
1448
2980
|
const raw = await this.get(`/payments/${paymentId}`);
|
|
1449
|
-
return
|
|
2981
|
+
return mapPaymentResult(raw, buildExplorerUrl);
|
|
1450
2982
|
}
|
|
1451
2983
|
/**
|
|
1452
2984
|
* Get agent-actionable status for a payment intent or approval request.
|
|
@@ -1456,37 +2988,13 @@ var HavenClient = class {
|
|
|
1456
2988
|
*/
|
|
1457
2989
|
async getPaymentStatus(paymentId) {
|
|
1458
2990
|
const raw = await this.get(`/machine-payments/${paymentId}/status`);
|
|
1459
|
-
return
|
|
2991
|
+
return mapPaymentStatusResult(raw);
|
|
1460
2992
|
}
|
|
1461
2993
|
/**
|
|
1462
2994
|
* Get the agent identity tied to this API key.
|
|
1463
2995
|
*/
|
|
1464
2996
|
async getAgent() {
|
|
1465
|
-
|
|
1466
|
-
const request = this.fetchAgent();
|
|
1467
|
-
this.agentInFlight = request;
|
|
1468
|
-
request.finally(() => {
|
|
1469
|
-
this.agentInFlight = null;
|
|
1470
|
-
}).catch(() => {
|
|
1471
|
-
});
|
|
1472
|
-
return request;
|
|
1473
|
-
}
|
|
1474
|
-
agentInFlight = null;
|
|
1475
|
-
async fetchAgent() {
|
|
1476
|
-
const raw = await this.get("/machine-payments/agent");
|
|
1477
|
-
return {
|
|
1478
|
-
id: raw.id,
|
|
1479
|
-
name: raw.name,
|
|
1480
|
-
status: raw.status,
|
|
1481
|
-
safeAddress: raw.safe_address,
|
|
1482
|
-
delegateAddress: raw.delegate_address,
|
|
1483
|
-
chainId: raw.chain_id,
|
|
1484
|
-
// Defensive normalization, not trust: the backend contract is exactly
|
|
1485
|
-
// 'legacy' | 'delegation' (#1306), but an older/mismatched backend
|
|
1486
|
-
// during a rollout window should degrade to the wider legacy bucket
|
|
1487
|
-
// rather than propagate an unrecognized string.
|
|
1488
|
-
executionRail: raw.execution_rail === "delegation" ? "delegation" : "legacy"
|
|
1489
|
-
};
|
|
2997
|
+
return this.accountReads.getAgent();
|
|
1490
2998
|
}
|
|
1491
2999
|
/**
|
|
1492
3000
|
* One-shot "am I ready?" bootstrap: identity + live spend authority + a
|
|
@@ -1496,23 +3004,7 @@ var HavenClient = class {
|
|
|
1496
3004
|
* without two round trips and manual assembly.
|
|
1497
3005
|
*/
|
|
1498
3006
|
async getAgentSummary() {
|
|
1499
|
-
|
|
1500
|
-
this.getAgent(),
|
|
1501
|
-
this.getAllowances()
|
|
1502
|
-
]);
|
|
1503
|
-
const allowances = allowanceSummary.allowances.map((a) => {
|
|
1504
|
-
const token = resolveTokenFromAddress(a.tokenAddress);
|
|
1505
|
-
const remainingDisplay = token ? `${formatAtomicAmount(safeBigInt(a.onchain.remaining), token.decimals)} ${a.tokenSymbol}` : `${a.onchain.remaining} ${a.tokenSymbol} (atomic; unknown decimals)`;
|
|
1506
|
-
return {
|
|
1507
|
-
tokenSymbol: a.tokenSymbol,
|
|
1508
|
-
remainingAtomic: a.onchain.remaining,
|
|
1509
|
-
remainingDisplay,
|
|
1510
|
-
configuredAmount: a.configuredAmount,
|
|
1511
|
-
resetPeriodMin: a.resetPeriodMin,
|
|
1512
|
-
isResetPending: a.onchain.isResetPending
|
|
1513
|
-
};
|
|
1514
|
-
});
|
|
1515
|
-
return { ...agent, readiness: deriveReadiness(agent.status, allowances), allowances };
|
|
3007
|
+
return this.accountReads.getAgentSummary();
|
|
1516
3008
|
}
|
|
1517
3009
|
/**
|
|
1518
3010
|
* Sweep stranded USDC and ETH from the delegate EOA back to the originating Safe.
|
|
@@ -1524,68 +3016,7 @@ var HavenClient = class {
|
|
|
1524
3016
|
* Requires `chainRpcs` to be set for the agent's chain in `HavenClientConfig`.
|
|
1525
3017
|
*/
|
|
1526
3018
|
async sweepDelegate() {
|
|
1527
|
-
|
|
1528
|
-
throw new HavenSigningError("delegateKey is required for sweepDelegate.");
|
|
1529
|
-
}
|
|
1530
|
-
const agent = await this.getAgent();
|
|
1531
|
-
const { safeAddress, delegateAddress, chainId } = agent;
|
|
1532
|
-
if (!delegateAddress) {
|
|
1533
|
-
throw new HavenApiError("Agent has no delegate address.", 422);
|
|
1534
|
-
}
|
|
1535
|
-
const rpcUrl = this.chainRpcs[chainId];
|
|
1536
|
-
if (!rpcUrl) {
|
|
1537
|
-
throw new HavenApiError(
|
|
1538
|
-
`chainRpcs[${chainId}] must be configured to sweep the delegate wallet.`,
|
|
1539
|
-
422
|
|
1540
|
-
);
|
|
1541
|
-
}
|
|
1542
|
-
const provider = createJsonRpcProvider(rpcUrl);
|
|
1543
|
-
const wallet = createWallet(this.delegateKey, provider);
|
|
1544
|
-
const ERC20_TRANSFER_ABI = ["function balanceOf(address) view returns (uint256)", "function transfer(address to, uint256 amount) returns (bool)"];
|
|
1545
|
-
const transfers = [];
|
|
1546
|
-
const usdcAddress = CHAIN_USDC[chainId];
|
|
1547
|
-
if (usdcAddress) {
|
|
1548
|
-
const usdcContract = createErc20Contract(usdcAddress, ERC20_TRANSFER_ABI, wallet);
|
|
1549
|
-
const usdcBalance = await usdcContract.balanceOf(delegateAddress);
|
|
1550
|
-
if (usdcBalance > 0n) {
|
|
1551
|
-
const tx = await usdcContract.transfer(safeAddress, usdcBalance);
|
|
1552
|
-
const receipt = await tx.wait(1);
|
|
1553
|
-
const txHash = receipt?.hash ?? tx.hash;
|
|
1554
|
-
transfers.push({
|
|
1555
|
-
asset: "USDC",
|
|
1556
|
-
amount: formatAtomicAmount(usdcBalance, 6),
|
|
1557
|
-
amountAtomic: usdcBalance.toString(),
|
|
1558
|
-
txHash,
|
|
1559
|
-
explorerUrl: buildExplorerUrl(chainId, txHash)
|
|
1560
|
-
});
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
1563
|
-
const ethBalance = await provider.getBalance(delegateAddress);
|
|
1564
|
-
if (ethBalance > 0n) {
|
|
1565
|
-
const fee = await provider.getFeeData();
|
|
1566
|
-
const effectiveGasPrice = fee.maxFeePerGas ?? fee.gasPrice ?? 1000000n;
|
|
1567
|
-
const gasLimit = 21000n;
|
|
1568
|
-
const gasCost = effectiveGasPrice * gasLimit * 2n;
|
|
1569
|
-
const ethToSend = ethBalance > gasCost ? ethBalance - gasCost : 0n;
|
|
1570
|
-
if (ethToSend > 0n) {
|
|
1571
|
-
const tx = await wallet.sendTransaction({ to: safeAddress, value: ethToSend });
|
|
1572
|
-
const receipt = await tx.wait(1);
|
|
1573
|
-
const txHash = receipt?.hash ?? tx.hash;
|
|
1574
|
-
transfers.push({
|
|
1575
|
-
asset: "ETH",
|
|
1576
|
-
amount: formatAtomicAmount(ethToSend, 18),
|
|
1577
|
-
amountAtomic: ethToSend.toString(),
|
|
1578
|
-
txHash,
|
|
1579
|
-
explorerUrl: buildExplorerUrl(chainId, txHash)
|
|
1580
|
-
});
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
return {
|
|
1584
|
-
fromAddress: delegateAddress,
|
|
1585
|
-
toAddress: safeAddress,
|
|
1586
|
-
chainId,
|
|
1587
|
-
transfers
|
|
1588
|
-
};
|
|
3019
|
+
return this.delegateSweep.sweepDelegate();
|
|
1589
3020
|
}
|
|
1590
3021
|
/**
|
|
1591
3022
|
* Hosted (keyless) split-signer sweep — step 1 of 2.
|
|
@@ -1596,7 +3027,7 @@ var HavenClient = class {
|
|
|
1596
3027
|
* edge signer's `haven_sign_sweep_delegate`. No key is required on this client.
|
|
1597
3028
|
*/
|
|
1598
3029
|
async prepareSweep() {
|
|
1599
|
-
return this.
|
|
3030
|
+
return this.delegateSweep.prepareSweep();
|
|
1600
3031
|
}
|
|
1601
3032
|
/**
|
|
1602
3033
|
* Hosted (keyless) split-signer sweep — step 2 of 2.
|
|
@@ -1606,40 +3037,13 @@ var HavenClient = class {
|
|
|
1606
3037
|
* the key.
|
|
1607
3038
|
*/
|
|
1608
3039
|
async submitSweep(authorization, signature) {
|
|
1609
|
-
return this.
|
|
1610
|
-
authorization,
|
|
1611
|
-
signature
|
|
1612
|
-
});
|
|
3040
|
+
return this.delegateSweep.submitSweep(authorization, signature);
|
|
1613
3041
|
}
|
|
1614
3042
|
/**
|
|
1615
3043
|
* Get configured and on-chain allowances for the authenticated agent.
|
|
1616
3044
|
*/
|
|
1617
3045
|
async getAllowances() {
|
|
1618
|
-
|
|
1619
|
-
return {
|
|
1620
|
-
agentId: raw.agent_id,
|
|
1621
|
-
safeAddress: raw.safe_address,
|
|
1622
|
-
delegateAddress: raw.delegate_address,
|
|
1623
|
-
chainId: raw.chain_id,
|
|
1624
|
-
allowances: raw.allowances.map((allowance) => ({
|
|
1625
|
-
id: allowance.id,
|
|
1626
|
-
tokenAddress: allowance.token_address,
|
|
1627
|
-
tokenSymbol: allowance.token_symbol,
|
|
1628
|
-
configuredAmount: allowance.configured_amount,
|
|
1629
|
-
resetPeriodMin: allowance.reset_period_min,
|
|
1630
|
-
onchain: {
|
|
1631
|
-
amount: allowance.onchain.amount,
|
|
1632
|
-
spent: allowance.onchain.spent,
|
|
1633
|
-
remaining: allowance.onchain.remaining,
|
|
1634
|
-
effectiveSpent: allowance.onchain.effective_spent,
|
|
1635
|
-
resetTimeMin: allowance.onchain.reset_time_min,
|
|
1636
|
-
lastResetMin: allowance.onchain.last_reset_min,
|
|
1637
|
-
nonce: allowance.onchain.nonce,
|
|
1638
|
-
isResetPending: allowance.onchain.is_reset_pending,
|
|
1639
|
-
remainingIsFromChain: allowance.onchain.remaining_is_from_chain
|
|
1640
|
-
}
|
|
1641
|
-
}))
|
|
1642
|
-
};
|
|
3046
|
+
return this.accountReads.getAllowances();
|
|
1643
3047
|
}
|
|
1644
3048
|
/**
|
|
1645
3049
|
* Post-purchase allowance/budget summary for a settled payment (#1310).
|
|
@@ -1670,62 +3074,7 @@ var HavenClient = class {
|
|
|
1670
3074
|
* phrase it as guaranteed-fresh.
|
|
1671
3075
|
*/
|
|
1672
3076
|
async getPostPurchaseAllowanceSummary(paymentId) {
|
|
1673
|
-
|
|
1674
|
-
payment: null,
|
|
1675
|
-
allowance: null,
|
|
1676
|
-
warnings: [
|
|
1677
|
-
{
|
|
1678
|
-
code: AgentPaymentWarningCode.AllowanceCheckUnavailable,
|
|
1679
|
-
message: `Could not read the post-purchase allowance/budget for payment ${paymentId} (${detail}). The payment itself succeeded \u2014 the on-chain policy remains the actual spend gate; this only affects the remaining-budget figure reported here.`
|
|
1680
|
-
}
|
|
1681
|
-
]
|
|
1682
|
-
});
|
|
1683
|
-
const [statusResult, agentResult, allowanceResult] = await Promise.allSettled([
|
|
1684
|
-
this.getPaymentStatus(paymentId),
|
|
1685
|
-
this.getAgent(),
|
|
1686
|
-
this.getAllowances()
|
|
1687
|
-
]);
|
|
1688
|
-
if (statusResult.status === "rejected") {
|
|
1689
|
-
return unavailable(statusResult.reason instanceof Error ? statusResult.reason.message : String(statusResult.reason));
|
|
1690
|
-
}
|
|
1691
|
-
const status = statusResult.value;
|
|
1692
|
-
if (agentResult.status === "rejected") {
|
|
1693
|
-
return { ...unavailable(agentResult.reason instanceof Error ? agentResult.reason.message : String(agentResult.reason)), payment: status };
|
|
1694
|
-
}
|
|
1695
|
-
if (allowanceResult.status === "rejected") {
|
|
1696
|
-
return { ...unavailable(allowanceResult.reason instanceof Error ? allowanceResult.reason.message : String(allowanceResult.reason)), payment: status };
|
|
1697
|
-
}
|
|
1698
|
-
try {
|
|
1699
|
-
const tokenAddress = status.asset ?? status.x402?.asset ?? null;
|
|
1700
|
-
if (!tokenAddress) {
|
|
1701
|
-
return { ...unavailable("the settled payment does not carry a resolvable token address"), payment: status };
|
|
1702
|
-
}
|
|
1703
|
-
const rail = agentResult.value.executionRail;
|
|
1704
|
-
const source = rail === "delegation" ? "active_delegations" : "allowance_module";
|
|
1705
|
-
const match = allowanceResult.value.allowances.find(
|
|
1706
|
-
(a) => a.tokenAddress.toLowerCase() === tokenAddress.toLowerCase()
|
|
1707
|
-
);
|
|
1708
|
-
if (!match) {
|
|
1709
|
-
return { ...unavailable("no allowance/budget row matches the settled token"), payment: status };
|
|
1710
|
-
}
|
|
1711
|
-
const token = resolveTokenFromAddress(match.tokenAddress);
|
|
1712
|
-
const remainingDisplay = token ? `${formatAtomicAmount(safeBigInt(match.onchain.remaining), token.decimals)} ${match.tokenSymbol}` : void 0;
|
|
1713
|
-
return {
|
|
1714
|
-
payment: status,
|
|
1715
|
-
allowance: {
|
|
1716
|
-
rail,
|
|
1717
|
-
remaining_atomic: match.onchain.remaining,
|
|
1718
|
-
...remainingDisplay ? { remaining_display: remainingDisplay } : {},
|
|
1719
|
-
token_symbol: match.tokenSymbol,
|
|
1720
|
-
token_address: match.tokenAddress,
|
|
1721
|
-
reset_period: match.resetPeriodMin,
|
|
1722
|
-
source
|
|
1723
|
-
},
|
|
1724
|
-
warnings: []
|
|
1725
|
-
};
|
|
1726
|
-
} catch (err) {
|
|
1727
|
-
return unavailable(err instanceof Error ? err.message : String(err));
|
|
1728
|
-
}
|
|
3077
|
+
return this.accountReads.getPostPurchaseAllowanceSummary(paymentId);
|
|
1729
3078
|
}
|
|
1730
3079
|
/**
|
|
1731
3080
|
* `haven_get_payment_status` convenience: fetch status and, for a
|
|
@@ -1782,9 +3131,7 @@ var HavenClient = class {
|
|
|
1782
3131
|
* List recent machine-payment receipts/evidence for bookkeeping.
|
|
1783
3132
|
*/
|
|
1784
3133
|
async listReceipts(options = {}) {
|
|
1785
|
-
|
|
1786
|
-
const raw = await this.get(`/machine-payments/receipts${query}`);
|
|
1787
|
-
return raw.receipts.map((receipt) => this.mapPaymentReceipt(receipt));
|
|
3134
|
+
return this.accountReads.listReceipts(options);
|
|
1788
3135
|
}
|
|
1789
3136
|
/**
|
|
1790
3137
|
* Fetch the verifiable receipt bundle for a settled payment and verify it
|
|
@@ -1793,10 +3140,7 @@ var HavenClient = class {
|
|
|
1793
3140
|
* authorisation, so the result is trustworthy even if the backend lied.
|
|
1794
3141
|
*/
|
|
1795
3142
|
async getReceipt(paymentId) {
|
|
1796
|
-
|
|
1797
|
-
`/payments/${paymentId}/receipt`
|
|
1798
|
-
);
|
|
1799
|
-
return { receipt, verification: verifyPaymentReceipt(receipt) };
|
|
3143
|
+
return this.accountReads.getReceipt(paymentId);
|
|
1800
3144
|
}
|
|
1801
3145
|
/**
|
|
1802
3146
|
* Rehydrate the x402 resume-state bundle for a payment id (#1328: the MPP
|
|
@@ -1849,17 +3193,16 @@ var HavenClient = class {
|
|
|
1849
3193
|
);
|
|
1850
3194
|
}
|
|
1851
3195
|
const idempotencyKey = options.idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option);
|
|
1852
|
-
const cached = this.
|
|
1853
|
-
if (cached
|
|
3196
|
+
const cached = this.fundingLeg.cachedReceipt(idempotencyKey);
|
|
3197
|
+
if (cached) return cached;
|
|
1854
3198
|
const inFlight = this.inFlightX402.get(idempotencyKey);
|
|
1855
3199
|
if (inFlight) return inFlight;
|
|
1856
|
-
const promise = this.
|
|
3200
|
+
const promise = this.fundingLeg.authorize(paymentRequired, option, idempotencyKey);
|
|
1857
3201
|
this.inFlightX402.set(idempotencyKey, promise);
|
|
1858
3202
|
try {
|
|
1859
3203
|
return await promise;
|
|
1860
3204
|
} catch (err) {
|
|
1861
|
-
|
|
1862
|
-
rail: "x402",
|
|
3205
|
+
attachResumeState(err, {
|
|
1863
3206
|
paymentRequired,
|
|
1864
3207
|
accepted: option,
|
|
1865
3208
|
idempotencyKey
|
|
@@ -1874,9 +3217,9 @@ var HavenClient = class {
|
|
|
1874
3217
|
* payment or approval request.
|
|
1875
3218
|
*/
|
|
1876
3219
|
async quoteX402(url, init, options = {}) {
|
|
1877
|
-
const initialInit =
|
|
1878
|
-
const request =
|
|
1879
|
-
const response = await this.
|
|
3220
|
+
const initialInit = withX402Wallet(init, x402PayerAddress(this.delegateAddress, this.x402Wallet));
|
|
3221
|
+
const request = snapshotX402Request(url, initialInit);
|
|
3222
|
+
const response = await this.merchantTransport.fetch(url, initialInit);
|
|
1880
3223
|
if (response.status !== 402) {
|
|
1881
3224
|
throw new X402UnexpectedStatusError(
|
|
1882
3225
|
`Expected an x402 quote response with HTTP 402, got HTTP ${response.status}.`,
|
|
@@ -1887,8 +3230,8 @@ var HavenClient = class {
|
|
|
1887
3230
|
throw new HavenApiError("quoteX402 only supports standard x402 Payment Required responses.", 400);
|
|
1888
3231
|
}
|
|
1889
3232
|
const paymentRequired = await parsePaymentRequiredResponse(response);
|
|
1890
|
-
const mcpTransport = await this.
|
|
1891
|
-
return
|
|
3233
|
+
const mcpTransport = await this.merchantTransport.detect(url, paymentRequired, response);
|
|
3234
|
+
return buildX402Quote(paymentRequired, request, options.idempotencyKey, mcpTransport);
|
|
1892
3235
|
}
|
|
1893
3236
|
/**
|
|
1894
3237
|
* Probe an MCP tool for its x402 quote without creating a payment.
|
|
@@ -1901,8 +3244,8 @@ var HavenClient = class {
|
|
|
1901
3244
|
* callers that need a plain x402 endpoint must use {@link quoteX402}.
|
|
1902
3245
|
*/
|
|
1903
3246
|
async quoteMcpX402(url, init, options = {}) {
|
|
1904
|
-
const wallet = await this.
|
|
1905
|
-
const sessionId = await this.
|
|
3247
|
+
const wallet = await this.merchantCompletion.resolveWalletForMerchantCall();
|
|
3248
|
+
const sessionId = await this.merchantTransport.initialize(url, init, wallet);
|
|
1906
3249
|
if (!sessionId) {
|
|
1907
3250
|
throw new HavenApiError(
|
|
1908
3251
|
"The merchant did not establish an MCP session before the x402 quote. No payment was created.",
|
|
@@ -1910,235 +3253,71 @@ var HavenClient = class {
|
|
|
1910
3253
|
{ mcpSessionNotEstablished: true }
|
|
1911
3254
|
);
|
|
1912
3255
|
}
|
|
1913
|
-
let requestInit =
|
|
1914
|
-
requestInit = this.
|
|
3256
|
+
let requestInit = withX402Wallet(init, wallet);
|
|
3257
|
+
requestInit = this.merchantTransport.withSessionHeaders(requestInit, sessionId);
|
|
1915
3258
|
const quote = await this.quoteX402(url, requestInit, options);
|
|
1916
3259
|
return {
|
|
1917
3260
|
...quote,
|
|
1918
3261
|
mcpTransport: quote.mcpTransport ?? { handshakeRequired: true, source: "path" }
|
|
1919
3262
|
};
|
|
1920
3263
|
}
|
|
1921
|
-
/**
|
|
1922
|
-
* Pay a previously inspected x402 quote and retry the exact captured request.
|
|
1923
|
-
*/
|
|
1924
|
-
async payX402Quote(quote, options = {}) {
|
|
1925
|
-
const idempotencyKey = options.idempotencyKey ?? quote.idempotencyKey;
|
|
1926
|
-
try {
|
|
1927
|
-
const receipt = await this.authorizeX402(quote.paymentRequired, { idempotencyKey });
|
|
1928
|
-
return this.
|
|
1929
|
-
quote.request.url,
|
|
1930
|
-
|
|
1931
|
-
quote.paymentRequired,
|
|
1932
|
-
receipt
|
|
1933
|
-
);
|
|
1934
|
-
} catch (err) {
|
|
1935
|
-
this.attachResumeState(err, {
|
|
1936
|
-
rail: "x402",
|
|
1937
|
-
paymentRequired: quote.paymentRequired,
|
|
1938
|
-
accepted: quote.accepted,
|
|
1939
|
-
idempotencyKey,
|
|
1940
|
-
request: quote.request
|
|
1941
|
-
});
|
|
1942
|
-
throw err;
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
async authorizeStandardX402(paymentRequired, option, idempotencyKey) {
|
|
1946
|
-
const raw = await this.post("/x402", {
|
|
1947
|
-
url: paymentRequired.resource.url,
|
|
1948
|
-
payTo: this.delegateAddress,
|
|
1949
|
-
merchantPayTo: option.payTo,
|
|
1950
|
-
amount: x402AuthorizationAmount(option),
|
|
1951
|
-
asset: option.asset,
|
|
1952
|
-
network: option.network,
|
|
1953
|
-
description: paymentRequired.resource.description,
|
|
1954
|
-
idempotencyKey,
|
|
1955
|
-
// #1360: same explicit funding-leg declaration as createX402Intent —
|
|
1956
|
-
// this local-key path derives payTo from the key (never stale), but the
|
|
1957
|
-
// declaration keeps both writers of the 3009 shape loud-by-default.
|
|
1958
|
-
settlementScheme: "eip3009"
|
|
1959
|
-
});
|
|
1960
|
-
const state = this.paymentStateFromRaw("x402 payment", raw);
|
|
1961
|
-
const executedReplay = raw.success && raw.tx_hash ? "idempotency-collision" : state?.nextAction === AgentPaymentNextAction.RetryOriginalX402Request ? "approval-resume" : null;
|
|
1962
|
-
if (executedReplay) {
|
|
1963
|
-
const canFund = await this.delegateCanFund(
|
|
1964
|
-
raw.chain_id ?? state?.chainId ?? chainIdFromNetwork(option.network),
|
|
1965
|
-
option.asset,
|
|
1966
|
-
x402AuthorizationAmount(option)
|
|
1967
|
-
);
|
|
1968
|
-
const refuse = executedReplay === "idempotency-collision" ? canFund !== true : canFund === false;
|
|
1969
|
-
if (refuse) {
|
|
1970
|
-
const settledReceipt = state && executedReplay === "approval-resume" ? this.mapX402ReceiptFromStatus(paymentRequired, option, void 0, state) : this.mapX402ReceiptFromAuthorization(paymentRequired, option, void 0, raw);
|
|
1971
|
-
throw new X402AlreadySettledError(
|
|
1972
|
-
canFund === false ? "This x402 payment already settled \u2014 the delegate no longer holds the funds to authorize it again. To buy the same item a second time, pass a distinct `idempotencyKey`; the synthesised key intentionally collapses repeat calls for the same product within a 5-minute window so a retried request cannot pay twice." : "This x402 payment already settled, and whether the delegate can still fund a new authorization could not be verified (no `chainRpcs` entry for this chain). Refusing rather than issue an authorization that may be unfundable. To buy the same item a second time, pass a distinct `idempotencyKey`; to finish an interrupted payment, resume it by `paymentId`.",
|
|
1973
|
-
settledReceipt,
|
|
1974
|
-
canFund === false ? "settled" : "unverifiable"
|
|
1975
|
-
);
|
|
1976
|
-
}
|
|
1977
|
-
}
|
|
1978
|
-
const paymentHeader = await this.createStandardX402Header(paymentRequired, option);
|
|
1979
|
-
if (raw.success && raw.tx_hash) {
|
|
1980
|
-
const receipt2 = this.mapX402ReceiptFromAuthorization(paymentRequired, option, paymentHeader, raw);
|
|
1981
|
-
this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt2);
|
|
1982
|
-
return receipt2;
|
|
1983
|
-
}
|
|
1984
|
-
if (state?.nextAction === AgentPaymentNextAction.RetryOriginalX402Request) {
|
|
1985
|
-
const receipt2 = this.mapX402ReceiptFromStatus(paymentRequired, option, paymentHeader, state);
|
|
1986
|
-
this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt2);
|
|
1987
|
-
return receipt2;
|
|
1988
|
-
}
|
|
1989
|
-
this.throwIfNonSignableAuthorizationState("x402 payment", raw);
|
|
1990
|
-
if (!raw.sign_data?.hash) {
|
|
1991
|
-
throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
|
|
1992
|
-
}
|
|
1993
|
-
const sig = await this.signForData(raw.sign_data);
|
|
1994
|
-
const execResult = await this.post(
|
|
1995
|
-
`/payments/${raw.payment_id}/sign`,
|
|
1996
|
-
{ signature: sig }
|
|
1997
|
-
);
|
|
1998
|
-
if (execResult.status !== "confirmed") {
|
|
1999
|
-
this.throwPaymentStateError("x402 payment", execResult);
|
|
2000
|
-
}
|
|
2001
|
-
await this.waitForFundingTx(
|
|
2002
|
-
execResult.tx_hash,
|
|
2003
|
-
execResult.chain_id ?? chainIdFromNetwork(option.network)
|
|
2004
|
-
);
|
|
2005
|
-
const receipt = this.mapX402ReceiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult);
|
|
2006
|
-
this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt);
|
|
2007
|
-
return receipt;
|
|
2008
|
-
}
|
|
2009
|
-
/**
|
|
2010
|
-
* Pay a merchant through **erc7710 direct settlement** (#1454, epic #1450).
|
|
2011
|
-
*
|
|
2012
|
-
* The whole point of this path is what it does NOT do. There is no funding
|
|
2013
|
-
* leg: the merchant redeems a delegation chain and pulls from the treasury
|
|
2014
|
-
* directly, so the delegate EOA never holds the money, no sweep can strand
|
|
2015
|
-
* it, and the #713 reconciliation class does not apply. It is also why this
|
|
2016
|
-
* method is SMALLER than the 3009 path — the backend assembles the merchant
|
|
2017
|
-
* `X-PAYMENT` header in `assembleSettlementPayload`, so the SDK builds no
|
|
2018
|
-
* header locally.
|
|
2019
|
-
*
|
|
2020
|
-
* authorize (payTo = the MERCHANT) → sign the child → settle → header
|
|
2021
|
-
*
|
|
2022
|
-
* The caller then retries the merchant with that header. **Nothing has
|
|
2023
|
-
* settled when this returns** — that is why it does not return an
|
|
2024
|
-
* `X402Receipt`.
|
|
2025
|
-
*
|
|
2026
|
-
* Requires a delegation-rail account. The backend enforces that
|
|
2027
|
-
* (`validateGenericSchemeRail`), and so does this method, before building a
|
|
2028
|
-
* request the backend would only reject: an error a client can explain is
|
|
2029
|
-
* worth more than a 400 it has to decode.
|
|
2030
|
-
*
|
|
2031
|
-
* **MCP callers must pass `options.resourceUrl`.** An in-band MCP 402
|
|
2032
|
-
* challenge frequently carries no `resource` object at all, so
|
|
2033
|
-
* `paymentRequired.resource?.url` is undefined and the backend answers
|
|
2034
|
-
* "Valid url is required". The QA scenario this path was ported from falls
|
|
2035
|
-
* back to the request URL for exactly that reason — the SDK cannot, because
|
|
2036
|
-
* it never saw the request. Pass it.
|
|
2037
|
-
*/
|
|
2038
|
-
async settleX402Erc7710(paymentRequired, options = {}) {
|
|
2039
|
-
if (!this.delegateKey) {
|
|
2040
|
-
throw new HavenSigningError(
|
|
2041
|
-
"delegateKey is required for x402 payments. Pass it in the HavenClient config."
|
|
2042
|
-
);
|
|
2043
|
-
}
|
|
2044
|
-
const prepared = await this.prepareX402Erc7710(paymentRequired, options);
|
|
2045
|
-
const signature = await this.signForData(prepared.signData);
|
|
2046
|
-
const paymentHeader = await this.submitX402Erc7710(prepared.paymentId, signature);
|
|
2047
|
-
return { ...prepared.settlement, paymentHeader };
|
|
2048
|
-
}
|
|
2049
|
-
/**
|
|
2050
|
-
* The AUTHORIZE half of erc7710 settlement (#1456): select the scheme, build
|
|
2051
|
-
* the request, and return the child to be signed — without signing it.
|
|
2052
|
-
*
|
|
2053
|
-
* Split out because the hosted topology cannot use `settleX402Erc7710()`:
|
|
2054
|
-
* that method signs in-process with `delegateKey`, and hosted Haven does not
|
|
2055
|
-
* have one and must not. The hosted MCP server drives these two halves with
|
|
2056
|
-
* the LOCAL signer in between, so the key stays where it belongs and the
|
|
2057
|
-
* request shaping stays in one place rather than being reimplemented.
|
|
2058
|
-
*/
|
|
2059
|
-
async prepareX402Erc7710(paymentRequired, options = {}) {
|
|
2060
|
-
const delegationRail = options.delegationRail ?? (await this.getAgent()).executionRail === "delegation";
|
|
2061
|
-
if (!delegationRail) {
|
|
2062
|
-
throw new HavenApiError(
|
|
2063
|
-
"erc7710 settlement requires a delegation-rail account; this one is not on it. Use authorizeX402() for the standard EIP-3009 path.",
|
|
2064
|
-
400
|
|
2065
|
-
);
|
|
2066
|
-
}
|
|
2067
|
-
const selection = selectX402SettlementScheme(paymentRequired.accepts, { delegationRail });
|
|
2068
|
-
if (!selection || selection.scheme !== "erc7710") {
|
|
2069
|
-
throw new HavenApiError(
|
|
2070
|
-
"This merchant does not advertise an erc7710 settlement option (no accepts[] entry carries extra.assetTransferMethod: 'erc7710'). Use authorizeX402() for the standard EIP-3009 path.",
|
|
2071
|
-
400
|
|
2072
|
-
);
|
|
2073
|
-
}
|
|
2074
|
-
const option = selection.option;
|
|
2075
|
-
const merchantPayTo = option.payTo;
|
|
2076
|
-
const amountAtomic = x402AuthorizationAmount(option);
|
|
2077
|
-
const raw = await this.post("/x402", {
|
|
2078
|
-
url: options.resourceUrl ?? paymentRequired.resource?.url,
|
|
2079
|
-
// payTo = the MERCHANT is what selects direct settlement server-side.
|
|
2080
|
-
// The explicit settlementScheme must AGREE with that shape (#1360) —
|
|
2081
|
-
// disagreement is a 400 by design, so that a stale delegate address
|
|
2082
|
-
// becomes a loud mismatch instead of a silent reroute to the 3009 leg.
|
|
2083
|
-
payTo: merchantPayTo,
|
|
2084
|
-
settlementScheme: "erc7710",
|
|
2085
|
-
amount: amountAtomic,
|
|
2086
|
-
asset: option.asset,
|
|
2087
|
-
network: option.network,
|
|
2088
|
-
// The v2 header echoes the accepted entry field-for-field, so the quoted
|
|
2089
|
-
// timeout must round-trip or the merchant rejects the echo (#1064).
|
|
2090
|
-
maxTimeoutSeconds: option.maxTimeoutSeconds,
|
|
2091
|
-
// #1058: forward the advertised facilitators verbatim — the child becomes
|
|
2092
|
-
// redeemable ONLY by them. `null` here means the merchant advertised none
|
|
2093
|
-
// (or an empty array, which the backend 400s on), so the field is OMITTED
|
|
2094
|
-
// rather than sent empty. See x402FacilitatorAddresses.
|
|
2095
|
-
...selection.facilitatorAddresses ? { facilitatorAddresses: selection.facilitatorAddresses } : {}
|
|
2096
|
-
});
|
|
2097
|
-
if (!raw.payment_id) {
|
|
2098
|
-
throw new HavenApiError("No payment_id returned from x402/authorize", 500, raw);
|
|
2099
|
-
}
|
|
2100
|
-
const signData = raw.sign_data;
|
|
2101
|
-
if (signData?.signature_scheme !== "eip712_delegation" || !signData.typed_data) {
|
|
2102
|
-
throw new HavenApiError(
|
|
2103
|
-
`x402/authorize did not return an erc7710 settlement child (signature_scheme was ${JSON.stringify(signData?.signature_scheme)}). Refusing to sign a payload this path did not ask for.`,
|
|
2104
|
-
500,
|
|
2105
|
-
raw
|
|
3264
|
+
/**
|
|
3265
|
+
* Pay a previously inspected x402 quote and retry the exact captured request.
|
|
3266
|
+
*/
|
|
3267
|
+
async payX402Quote(quote, options = {}) {
|
|
3268
|
+
const idempotencyKey = options.idempotencyKey ?? quote.idempotencyKey;
|
|
3269
|
+
try {
|
|
3270
|
+
const receipt = await this.authorizeX402(quote.paymentRequired, { idempotencyKey });
|
|
3271
|
+
return this.merchantCompletion.retryRequest(
|
|
3272
|
+
quote.request.url,
|
|
3273
|
+
requestInitFromSnapshot(quote.request),
|
|
3274
|
+
quote.paymentRequired,
|
|
3275
|
+
receipt
|
|
2106
3276
|
);
|
|
3277
|
+
} catch (err) {
|
|
3278
|
+
attachResumeState(err, {
|
|
3279
|
+
paymentRequired: quote.paymentRequired,
|
|
3280
|
+
accepted: quote.accepted,
|
|
3281
|
+
idempotencyKey,
|
|
3282
|
+
request: quote.request
|
|
3283
|
+
});
|
|
3284
|
+
throw err;
|
|
2107
3285
|
}
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
3286
|
+
}
|
|
3287
|
+
/**
|
|
3288
|
+
* Pay a merchant through **erc7710 direct settlement** (#1454, epic #1450).
|
|
3289
|
+
*
|
|
3290
|
+
* **Nothing has settled when this returns** — that is why it does not return
|
|
3291
|
+
* an `X402Receipt`; the caller still has to retry the merchant with the
|
|
3292
|
+
* header. **MCP callers must pass `options.resourceUrl`**, because an in-band
|
|
3293
|
+
* MCP 402 challenge frequently carries no `resource` object at all.
|
|
3294
|
+
*
|
|
3295
|
+
* Both caveats, and why this scheme has no funding leg, are explained where
|
|
3296
|
+
* the lifecycle lives: `x402-erc7710.ts` (#1619).
|
|
3297
|
+
*/
|
|
3298
|
+
async settleX402Erc7710(paymentRequired, options = {}) {
|
|
3299
|
+
return this.erc7710.settle(paymentRequired, options);
|
|
3300
|
+
}
|
|
3301
|
+
/**
|
|
3302
|
+
* The AUTHORIZE half of erc7710 settlement (#1456): select the scheme, build
|
|
3303
|
+
* the request, and return the child to be signed — without signing it.
|
|
3304
|
+
*
|
|
3305
|
+
* Split out because the hosted topology cannot use `settleX402Erc7710()`:
|
|
3306
|
+
* that method signs in-process with `delegateKey`, and hosted Haven does not
|
|
3307
|
+
* have one and must not.
|
|
3308
|
+
*/
|
|
3309
|
+
async prepareX402Erc7710(paymentRequired, options = {}) {
|
|
3310
|
+
return this.erc7710.prepare(paymentRequired, options);
|
|
2120
3311
|
}
|
|
2121
3312
|
/**
|
|
2122
3313
|
* The SETTLE half (#1456): exchange the signed child for the merchant header.
|
|
2123
3314
|
*
|
|
2124
3315
|
* The SDK builds no header on this path — the backend assembles the MetaMask
|
|
2125
|
-
* erc7710 payload
|
|
2126
|
-
*
|
|
2127
|
-
* hosted boundary) is irrelevant here.
|
|
3316
|
+
* erc7710 payload. Whoever produced the signature (an in-process delegate
|
|
3317
|
+
* key, or the local edge signer over the hosted boundary) is irrelevant.
|
|
2128
3318
|
*/
|
|
2129
3319
|
async submitX402Erc7710(paymentId, signature) {
|
|
2130
|
-
|
|
2131
|
-
`/x402/${paymentId}/settle`,
|
|
2132
|
-
{ signature }
|
|
2133
|
-
);
|
|
2134
|
-
if (!settled.payment_header) {
|
|
2135
|
-
throw new HavenApiError(
|
|
2136
|
-
"x402 settle returned no payment_header \u2014 the merchant cannot be retried.",
|
|
2137
|
-
500,
|
|
2138
|
-
settled
|
|
2139
|
-
);
|
|
2140
|
-
}
|
|
2141
|
-
return settled.payment_header;
|
|
3320
|
+
return this.erc7710.submit(paymentId, signature);
|
|
2142
3321
|
}
|
|
2143
3322
|
async resumeAuthorizedX402(input) {
|
|
2144
3323
|
if (!this.delegateKey) {
|
|
@@ -2157,11 +3336,11 @@ var HavenClient = class {
|
|
|
2157
3336
|
);
|
|
2158
3337
|
}
|
|
2159
3338
|
const idempotencyKey = input.idempotencyKey ?? buildX402IdempotencyKey(input.paymentRequired, option);
|
|
2160
|
-
const cached = this.
|
|
2161
|
-
if (cached
|
|
3339
|
+
const cached = this.fundingLeg.cachedReceipt(idempotencyKey);
|
|
3340
|
+
if (cached) return cached;
|
|
2162
3341
|
const status = await this.getPaymentStatus(input.paymentId);
|
|
2163
|
-
|
|
2164
|
-
const canFund = await this.delegateCanFund(
|
|
3342
|
+
assertCanResumeX402(status, input.paymentRequired, option);
|
|
3343
|
+
const canFund = await this.fundingLeg.delegateCanFund(
|
|
2165
3344
|
status.chainId ?? chainIdFromNetwork(option.network),
|
|
2166
3345
|
option.asset,
|
|
2167
3346
|
x402AuthorizationAmount(option)
|
|
@@ -2169,20 +3348,20 @@ var HavenClient = class {
|
|
|
2169
3348
|
if (canFund === false) {
|
|
2170
3349
|
throw new X402AlreadySettledError(
|
|
2171
3350
|
`x402 payment ${status.paymentId} has already settled \u2014 the delegate no longer holds the funds to authorize it again, so there is nothing left to resume.`,
|
|
2172
|
-
this.
|
|
3351
|
+
this.fundingLeg.receiptFromStatus(input.paymentRequired, option, void 0, status),
|
|
2173
3352
|
"settled"
|
|
2174
3353
|
);
|
|
2175
3354
|
}
|
|
2176
|
-
const paymentHeader = await this.
|
|
2177
|
-
const receipt = this.
|
|
2178
|
-
this.
|
|
3355
|
+
const paymentHeader = await this.fundingLeg.createPaymentHeader(input.paymentRequired, option);
|
|
3356
|
+
const receipt = this.fundingLeg.receiptFromStatus(input.paymentRequired, option, paymentHeader, status);
|
|
3357
|
+
this.fundingLeg.cacheReceipt(idempotencyKey, paymentHeader, receipt);
|
|
2179
3358
|
return receipt;
|
|
2180
3359
|
}
|
|
2181
3360
|
async resumeX402Payment(input) {
|
|
2182
3361
|
const inputInit = "init" in input ? input.init : void 0;
|
|
2183
|
-
const initialInit =
|
|
2184
|
-
inputInit ?? (input.request ?
|
|
2185
|
-
this.
|
|
3362
|
+
const initialInit = withX402Wallet(
|
|
3363
|
+
inputInit ?? (input.request ? requestInitFromSnapshot(input.request) : void 0),
|
|
3364
|
+
x402PayerAddress(this.delegateAddress, this.x402Wallet)
|
|
2186
3365
|
);
|
|
2187
3366
|
let paymentRequired = input.paymentRequired;
|
|
2188
3367
|
const url = input.url ?? input.request?.url;
|
|
@@ -2190,7 +3369,7 @@ var HavenClient = class {
|
|
|
2190
3369
|
if (!url) {
|
|
2191
3370
|
throw new HavenApiError("x402 resume requires the original URL or a captured request snapshot.", 400);
|
|
2192
3371
|
}
|
|
2193
|
-
const response = await this.
|
|
3372
|
+
const response = await this.merchantTransport.fetch(url, initialInit);
|
|
2194
3373
|
if (response.status !== 402) {
|
|
2195
3374
|
throw new HavenApiError("Expected the original x402 request to return HTTP 402 before resuming.", 400);
|
|
2196
3375
|
}
|
|
@@ -2201,7 +3380,7 @@ var HavenClient = class {
|
|
|
2201
3380
|
paymentRequired,
|
|
2202
3381
|
idempotencyKey: input.idempotencyKey
|
|
2203
3382
|
});
|
|
2204
|
-
return this.
|
|
3383
|
+
return this.merchantCompletion.retryRequest(url ?? paymentRequired.resource.url, initialInit, paymentRequired, receipt);
|
|
2205
3384
|
}
|
|
2206
3385
|
/**
|
|
2207
3386
|
* Fetch wrapper that automatically handles HTTP 402 responses.
|
|
@@ -2215,268 +3394,57 @@ var HavenClient = class {
|
|
|
2215
3394
|
* ```
|
|
2216
3395
|
*
|
|
2217
3396
|
* **MCP-over-x402 auto-handshake (issue #315):** when the endpoint is
|
|
2218
|
-
* MCP-shaped — the URL path ends in `/mcp`, or the 402 body carries a
|
|
2219
|
-
* Coinbase Bazaar `extensions.bazaar` block — the SDK runs the MCP
|
|
2220
|
-
* `initialize` handshake, threads the resulting `mcp-session-id`,
|
|
2221
|
-
* `Accept: application/json, text/event-stream`, and `x402-wallet` headers
|
|
2222
|
-
* through every request, and collapses SSE responses to the JSON-RPC
|
|
2223
|
-
* `result`. The caller just passes `(url, { body })` and never sees the
|
|
2224
|
-
* protocol plumbing. A non-MCP server (handshake error / no session id)
|
|
2225
|
-
* falls back to standard x402 behaviour.
|
|
2226
|
-
*
|
|
2227
|
-
* Requires `delegateKey` to be set in the client config.
|
|
2228
|
-
*/
|
|
2229
|
-
async fetch(url, init, options = {}) {
|
|
2230
|
-
let mcpSessionId;
|
|
2231
|
-
if (isMcpUrl(url)) {
|
|
2232
|
-
mcpSessionId = await this.mcpInitialize(url, init);
|
|
2233
|
-
}
|
|
2234
|
-
let requestInit = this.withX402Wallet(init, this.x402PayerAddress());
|
|
2235
|
-
if (mcpSessionId) requestInit = this.withMcpHeaders(requestInit, mcpSessionId);
|
|
2236
|
-
const response = await this.merchantFetch(url, requestInit);
|
|
2237
|
-
if (response.status !== 402) {
|
|
2238
|
-
return mcpSessionId ? this.surfaceMcpResult(response) : response;
|
|
2239
|
-
}
|
|
2240
|
-
let paymentRequired;
|
|
2241
|
-
try {
|
|
2242
|
-
paymentRequired = await parsePaymentRequiredResponse(response);
|
|
2243
|
-
} catch {
|
|
2244
|
-
return response;
|
|
2245
|
-
}
|
|
2246
|
-
if (!mcpSessionId && await responseHasBazaarExtension(response)) {
|
|
2247
|
-
mcpSessionId = await this.mcpInitialize(url, init);
|
|
2248
|
-
if (mcpSessionId) requestInit = this.withMcpHeaders(requestInit, mcpSessionId);
|
|
2249
|
-
}
|
|
2250
|
-
const request = this.snapshotX402Request(url, requestInit);
|
|
2251
|
-
const option = selectStandardPaymentOption(paymentRequired.accepts);
|
|
2252
|
-
const idempotencyKey = options.idempotencyKey ?? (option ? buildX402IdempotencyKey(paymentRequired, option) : void 0);
|
|
2253
|
-
let receipt;
|
|
2254
|
-
try {
|
|
2255
|
-
receipt = await this.authorizeX402(paymentRequired, options);
|
|
2256
|
-
} catch (err) {
|
|
2257
|
-
if (option && idempotencyKey) {
|
|
2258
|
-
this.attachResumeState(err, {
|
|
2259
|
-
rail: "x402",
|
|
2260
|
-
paymentRequired,
|
|
2261
|
-
accepted: option,
|
|
2262
|
-
idempotencyKey,
|
|
2263
|
-
request
|
|
2264
|
-
});
|
|
2265
|
-
}
|
|
2266
|
-
throw err;
|
|
2267
|
-
}
|
|
2268
|
-
const retryResponse = await this.retryX402Request(url, requestInit, paymentRequired, receipt);
|
|
2269
|
-
return mcpSessionId ? this.surfaceMcpResult(retryResponse) : retryResponse;
|
|
2270
|
-
}
|
|
2271
|
-
// ── MCP-over-x402 transport helpers (issue #315) ─────────────────
|
|
2272
|
-
/**
|
|
2273
|
-
* Run the MCP `initialize` handshake against a Streamable-HTTP endpoint and
|
|
2274
|
-
* return the `mcp-session-id` the server assigns.
|
|
2275
|
-
*
|
|
2276
|
-
* Returns `undefined` whenever the endpoint is not actually an MCP server —
|
|
2277
|
-
* a transport/HTTP error, a missing session id, or a JSON-RPC error in the
|
|
2278
|
-
* handshake response — so the caller can fall back to plain x402.
|
|
2279
|
-
*/
|
|
2280
|
-
async mcpInitialize(url, init, wallet = this.x402PayerAddress()) {
|
|
2281
|
-
try {
|
|
2282
|
-
const headers = new Headers(init?.headers);
|
|
2283
|
-
headers.set("Content-Type", "application/json");
|
|
2284
|
-
headers.set("Accept", MCP_ACCEPT);
|
|
2285
|
-
if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
|
|
2286
|
-
const response = await this.merchantFetch(url, {
|
|
2287
|
-
method: "POST",
|
|
2288
|
-
headers,
|
|
2289
|
-
body: JSON.stringify({
|
|
2290
|
-
jsonrpc: "2.0",
|
|
2291
|
-
id: ++this.mcpRequestId,
|
|
2292
|
-
method: "initialize",
|
|
2293
|
-
params: {
|
|
2294
|
-
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
2295
|
-
capabilities: {},
|
|
2296
|
-
clientInfo: MCP_CLIENT_INFO
|
|
2297
|
-
}
|
|
2298
|
-
})
|
|
2299
|
-
});
|
|
2300
|
-
if (!response.ok) return void 0;
|
|
2301
|
-
const sessionId = response.headers.get("mcp-session-id");
|
|
2302
|
-
if (!sessionId) return void 0;
|
|
2303
|
-
const message = await this.readMcpMessage(response);
|
|
2304
|
-
if (message && "error" in message) return void 0;
|
|
2305
|
-
await this.mcpNotifyInitialized(url, init, sessionId, wallet);
|
|
2306
|
-
return sessionId;
|
|
2307
|
-
} catch {
|
|
2308
|
-
return void 0;
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
/**
|
|
2312
|
-
* Send the MCP `notifications/initialized` notification that completes the
|
|
2313
|
-
* lifecycle handshake. Best-effort: the session is already established, so a
|
|
2314
|
-
* failed notification must not abort the payment.
|
|
2315
|
-
*/
|
|
2316
|
-
async mcpNotifyInitialized(url, init, sessionId, wallet = this.x402PayerAddress()) {
|
|
2317
|
-
try {
|
|
2318
|
-
const headers = new Headers(init?.headers);
|
|
2319
|
-
headers.set("Content-Type", "application/json");
|
|
2320
|
-
headers.set("Accept", MCP_ACCEPT);
|
|
2321
|
-
headers.set("mcp-session-id", sessionId);
|
|
2322
|
-
if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
|
|
2323
|
-
await this.merchantFetch(
|
|
2324
|
-
url,
|
|
2325
|
-
{
|
|
2326
|
-
method: "POST",
|
|
2327
|
-
headers,
|
|
2328
|
-
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
|
|
2329
|
-
},
|
|
2330
|
-
NOTIFY_TIMEOUT
|
|
2331
|
-
);
|
|
2332
|
-
} catch {
|
|
2333
|
-
}
|
|
2334
|
-
}
|
|
2335
|
-
/** Read a single JSON-RPC message from an MCP response (JSON or SSE body). */
|
|
2336
|
-
async readMcpMessage(response) {
|
|
2337
|
-
let text;
|
|
2338
|
-
try {
|
|
2339
|
-
text = await response.clone().text();
|
|
2340
|
-
} catch {
|
|
2341
|
-
return void 0;
|
|
2342
|
-
}
|
|
2343
|
-
if ((response.headers.get("content-type") ?? "").includes("text/event-stream")) {
|
|
2344
|
-
return selectJsonRpcResult(parseSseJsonRpcMessages(text));
|
|
2345
|
-
}
|
|
2346
|
-
try {
|
|
2347
|
-
return JSON.parse(text);
|
|
2348
|
-
} catch {
|
|
2349
|
-
return void 0;
|
|
2350
|
-
}
|
|
2351
|
-
}
|
|
2352
|
-
/** Add the MCP transport headers (session id + SSE Accept) to a request. */
|
|
2353
|
-
withMcpHeaders(init, sessionId) {
|
|
2354
|
-
const headers = new Headers(init?.headers);
|
|
2355
|
-
headers.set("mcp-session-id", sessionId);
|
|
2356
|
-
headers.set("Accept", MCP_ACCEPT);
|
|
2357
|
-
return { ...init, headers };
|
|
2358
|
-
}
|
|
2359
|
-
/**
|
|
2360
|
-
* Collapse an MCP SSE response into a plain JSON response carrying the
|
|
2361
|
-
* JSON-RPC `result`, so callers of `fetch()` never see raw SSE framing.
|
|
2362
|
-
* Non-SSE responses pass through untouched.
|
|
2363
|
-
*/
|
|
2364
|
-
async surfaceMcpResult(response) {
|
|
2365
|
-
if (!(response.headers.get("content-type") ?? "").includes("text/event-stream")) {
|
|
2366
|
-
return response;
|
|
2367
|
-
}
|
|
2368
|
-
let text;
|
|
2369
|
-
try {
|
|
2370
|
-
text = await response.clone().text();
|
|
2371
|
-
} catch {
|
|
2372
|
-
return response;
|
|
2373
|
-
}
|
|
2374
|
-
const message = selectJsonRpcResult(parseSseJsonRpcMessages(text));
|
|
2375
|
-
if (!message) return response;
|
|
2376
|
-
const body = "result" in message ? message.result : message;
|
|
2377
|
-
const headers = new Headers(response.headers);
|
|
2378
|
-
headers.set("content-type", "application/json");
|
|
2379
|
-
headers.delete("content-length");
|
|
2380
|
-
headers.delete("mcp-session-id");
|
|
2381
|
-
return new Response(JSON.stringify(body), {
|
|
2382
|
-
status: response.status,
|
|
2383
|
-
statusText: response.statusText,
|
|
2384
|
-
headers
|
|
2385
|
-
});
|
|
2386
|
-
}
|
|
2387
|
-
async retryX402Request(url, initialInit, paymentRequired, receipt) {
|
|
2388
|
-
if (!receipt.accepted) {
|
|
2389
|
-
throw new HavenApiError("No accepted x402 option was recorded for payment retry", 500);
|
|
2390
|
-
}
|
|
2391
|
-
if (!receipt.paymentHeader) {
|
|
2392
|
-
throw new HavenApiError("No x402 payment header was returned for payment retry", 500);
|
|
2393
|
-
}
|
|
2394
|
-
const retryHeaders = new Headers(initialInit?.headers);
|
|
2395
|
-
retryHeaders.set("X-PAYMENT", receipt.paymentHeader);
|
|
2396
|
-
const retryResponse = await this.merchantFetch(url, {
|
|
2397
|
-
...initialInit,
|
|
2398
|
-
headers: retryHeaders
|
|
2399
|
-
});
|
|
2400
|
-
if (!retryResponse.ok) {
|
|
2401
|
-
const merchant = await captureMerchantResponse(retryResponse);
|
|
2402
|
-
await this.recordMerchantRetryRejected({
|
|
2403
|
-
rail: "x402",
|
|
2404
|
-
paymentId: receipt.paymentId,
|
|
2405
|
-
txHash: receipt.txHash,
|
|
2406
|
-
resourceUrl: receipt.resourceUrl,
|
|
2407
|
-
merchant,
|
|
2408
|
-
details: {
|
|
2409
|
-
merchant_to: receipt.merchantTo,
|
|
2410
|
-
delegate_to: receipt.to
|
|
2411
|
-
}
|
|
2412
|
-
});
|
|
2413
|
-
throw new HavenApiError(
|
|
2414
|
-
"x402 retry failed after Haven funded the delegate wallet; reconciliation may be required.",
|
|
2415
|
-
merchant.merchant_status,
|
|
2416
|
-
{
|
|
2417
|
-
marker: "x402_retry_rejected_after_funding",
|
|
2418
|
-
payment_id: receipt.paymentId,
|
|
2419
|
-
tx_hash: receipt.txHash,
|
|
2420
|
-
resource_url: receipt.resourceUrl,
|
|
2421
|
-
merchant_to: receipt.merchantTo,
|
|
2422
|
-
delegate_to: receipt.to,
|
|
2423
|
-
...merchant
|
|
2424
|
-
}
|
|
2425
|
-
);
|
|
2426
|
-
}
|
|
2427
|
-
const merchantSettlement = parseMerchantSettlement(retryResponse.headers.get("PAYMENT-RESPONSE"));
|
|
2428
|
-
if (receipt.merchant && merchantSettlement.settlementTxHash) {
|
|
2429
|
-
receipt.merchant.settlementTxHash = merchantSettlement.settlementTxHash;
|
|
2430
|
-
receipt.merchant.settlementExplorerUrl = buildExplorerUrl(
|
|
2431
|
-
receipt.chainId,
|
|
2432
|
-
merchantSettlement.settlementTxHash
|
|
2433
|
-
);
|
|
2434
|
-
}
|
|
2435
|
-
await this.reportMachinePaymentEvidence({
|
|
2436
|
-
paymentId: receipt.paymentId,
|
|
2437
|
-
rail: "x402",
|
|
2438
|
-
txHash: receipt.txHash,
|
|
2439
|
-
resourceUrl: receipt.resourceUrl,
|
|
2440
|
-
merchantStatus: retryResponse.status,
|
|
2441
|
-
challengePayload: paymentRequired,
|
|
2442
|
-
selectedPayment: receipt.accepted,
|
|
2443
|
-
paymentProofHeaderName: "X-PAYMENT",
|
|
2444
|
-
paymentProofHeader: receipt.paymentHeader,
|
|
2445
|
-
protocolReceiptHeaderName: "PAYMENT-RESPONSE",
|
|
2446
|
-
protocolReceiptHeader: retryResponse.headers.get("PAYMENT-RESPONSE") ?? void 0
|
|
2447
|
-
});
|
|
2448
|
-
await this.reportMerchantReceipt(receipt.paymentId, retryResponse);
|
|
2449
|
-
return retryResponse;
|
|
2450
|
-
}
|
|
2451
|
-
/**
|
|
2452
|
-
* #956: capture the merchant's OWN receipt when the paid response carries
|
|
2453
|
-
* one, and report it to Haven so the reporting feed can attach it next to
|
|
2454
|
-
* the Haven-generated payment evidence (#498). Two supported signals on the
|
|
2455
|
-
* paid response:
|
|
2456
|
-
*
|
|
2457
|
-
* x-receipt-json: base64-encoded JSON receipt document (inline)
|
|
2458
|
-
* x-receipt-url: https URL to the receipt document (reference)
|
|
3397
|
+
* MCP-shaped — the URL path ends in `/mcp`, or the 402 body carries a
|
|
3398
|
+
* Coinbase Bazaar `extensions.bazaar` block — the SDK runs the MCP
|
|
3399
|
+
* `initialize` handshake, threads the resulting `mcp-session-id`,
|
|
3400
|
+
* `Accept: application/json, text/event-stream`, and `x402-wallet` headers
|
|
3401
|
+
* through every request, and collapses SSE responses to the JSON-RPC
|
|
3402
|
+
* `result`. The caller just passes `(url, { body })` and never sees the
|
|
3403
|
+
* protocol plumbing. A non-MCP server (handshake error / no session id)
|
|
3404
|
+
* falls back to standard x402 behaviour.
|
|
2459
3405
|
*
|
|
2460
|
-
*
|
|
2461
|
-
* ever affect the completed payment — the response is already paid for.
|
|
3406
|
+
* Requires `delegateKey` to be set in the client config.
|
|
2462
3407
|
*/
|
|
2463
|
-
async
|
|
3408
|
+
async fetch(url, init, options = {}) {
|
|
3409
|
+
let mcpSessionId;
|
|
3410
|
+
if (this.merchantTransport.isMcpUrl(url)) {
|
|
3411
|
+
mcpSessionId = await this.merchantTransport.initialize(url, init);
|
|
3412
|
+
}
|
|
3413
|
+
let requestInit = withX402Wallet(init, x402PayerAddress(this.delegateAddress, this.x402Wallet));
|
|
3414
|
+
if (mcpSessionId) requestInit = this.merchantTransport.withSessionHeaders(requestInit, mcpSessionId);
|
|
3415
|
+
const response = await this.merchantTransport.fetch(url, requestInit);
|
|
3416
|
+
if (response.status !== 402) {
|
|
3417
|
+
return mcpSessionId ? this.merchantTransport.surfaceResult(response) : response;
|
|
3418
|
+
}
|
|
3419
|
+
let paymentRequired;
|
|
2464
3420
|
try {
|
|
2465
|
-
|
|
2466
|
-
const url = response.headers.get("x-receipt-url");
|
|
2467
|
-
if (!inlineB64 && !url) return;
|
|
2468
|
-
let body = null;
|
|
2469
|
-
if (inlineB64) {
|
|
2470
|
-
if (inlineB64.length > Math.ceil(64 * 1024 * 4 / 3)) return;
|
|
2471
|
-
const decoded = JSON.parse(Buffer.from(inlineB64, "base64").toString("utf8"));
|
|
2472
|
-
if (decoded && typeof decoded === "object") body = { json: decoded };
|
|
2473
|
-
} else if (url && url.startsWith("https://") && url.length <= 2048) {
|
|
2474
|
-
body = { url };
|
|
2475
|
-
}
|
|
2476
|
-
if (!body) return;
|
|
2477
|
-
await this.post(`/machine-payments/${paymentId}/merchant-receipt`, body);
|
|
3421
|
+
paymentRequired = await parsePaymentRequiredResponse(response);
|
|
2478
3422
|
} catch {
|
|
3423
|
+
return response;
|
|
3424
|
+
}
|
|
3425
|
+
if (!mcpSessionId && await this.merchantTransport.hasBazaarExtension(response)) {
|
|
3426
|
+
mcpSessionId = await this.merchantTransport.initialize(url, init);
|
|
3427
|
+
if (mcpSessionId) requestInit = this.merchantTransport.withSessionHeaders(requestInit, mcpSessionId);
|
|
3428
|
+
}
|
|
3429
|
+
const request = snapshotX402Request(url, requestInit);
|
|
3430
|
+
const option = selectStandardPaymentOption(paymentRequired.accepts);
|
|
3431
|
+
const idempotencyKey = options.idempotencyKey ?? (option ? buildX402IdempotencyKey(paymentRequired, option) : void 0);
|
|
3432
|
+
let receipt;
|
|
3433
|
+
try {
|
|
3434
|
+
receipt = await this.authorizeX402(paymentRequired, options);
|
|
3435
|
+
} catch (err) {
|
|
3436
|
+
if (option && idempotencyKey) {
|
|
3437
|
+
attachResumeState(err, {
|
|
3438
|
+
paymentRequired,
|
|
3439
|
+
accepted: option,
|
|
3440
|
+
idempotencyKey,
|
|
3441
|
+
request
|
|
3442
|
+
});
|
|
3443
|
+
}
|
|
3444
|
+
throw err;
|
|
2479
3445
|
}
|
|
3446
|
+
const retryResponse = await this.merchantCompletion.retryRequest(url, requestInit, paymentRequired, receipt);
|
|
3447
|
+
return mcpSessionId ? this.merchantTransport.surfaceResult(retryResponse) : retryResponse;
|
|
2480
3448
|
}
|
|
2481
3449
|
/**
|
|
2482
3450
|
* Deliver an already-signed x402 payment header to the merchant and return
|
|
@@ -2501,7 +3469,7 @@ var HavenClient = class {
|
|
|
2501
3469
|
* and before delivering the X-PAYMENT header, so the merchant's
|
|
2502
3470
|
* balanceOf(delegate) / transferWithAuthorization verification sees the funded
|
|
2503
3471
|
* balance — otherwise it rejects with "Payment verification failed". The
|
|
2504
|
-
* SDK's local path already does this (see
|
|
3472
|
+
* SDK's local path already does this (see `X402FundingLeg.authorize`); the hosted
|
|
2505
3473
|
* split flow regressed when the 5→3 collapse removed the incidental
|
|
2506
3474
|
* inter-call latency that used to mask it.
|
|
2507
3475
|
*
|
|
@@ -2518,28 +3486,25 @@ var HavenClient = class {
|
|
|
2518
3486
|
*/
|
|
2519
3487
|
async ensureFundingConfirmed(paymentId, fundingTxHash) {
|
|
2520
3488
|
const status = await this.getPaymentStatus(paymentId);
|
|
2521
|
-
await this.waitForFundingTx(fundingTxHash ?? status.txHash ?? void 0, status.chainId);
|
|
3489
|
+
await this.fundingLeg.waitForFundingTx(fundingTxHash ?? status.txHash ?? void 0, status.chainId);
|
|
2522
3490
|
}
|
|
2523
3491
|
async completeX402MerchantCall(input) {
|
|
2524
|
-
const evidenceContext = await this.
|
|
3492
|
+
const evidenceContext = await this.merchantCompletion.resolveCompletionContext({
|
|
2525
3493
|
paymentId: input.paymentId,
|
|
2526
3494
|
url: input.url,
|
|
2527
3495
|
noFundingLeg: input.noFundingLeg === true
|
|
2528
3496
|
});
|
|
2529
3497
|
const fundingTxHash = evidenceContext.txHash;
|
|
2530
|
-
const shouldHandshakeMcp = isMcpUrl(input.url) || input.mcpTransport?.handshakeRequired === true;
|
|
2531
|
-
const x402Wallet = shouldHandshakeMcp ? await this.
|
|
3498
|
+
const shouldHandshakeMcp = this.merchantTransport.isMcpUrl(input.url) || input.mcpTransport?.handshakeRequired === true;
|
|
3499
|
+
const x402Wallet = shouldHandshakeMcp ? await this.merchantCompletion.resolveWalletForMerchantCall() : x402PayerAddress(this.delegateAddress, this.x402Wallet);
|
|
2532
3500
|
let mcpSessionId;
|
|
2533
3501
|
if (shouldHandshakeMcp) {
|
|
2534
|
-
mcpSessionId = await this.
|
|
3502
|
+
mcpSessionId = await this.merchantTransport.initialize(input.url, input.init, x402Wallet);
|
|
2535
3503
|
}
|
|
2536
|
-
let requestInit =
|
|
2537
|
-
if (mcpSessionId) requestInit = this.
|
|
2538
|
-
const
|
|
2539
|
-
|
|
2540
|
-
requestInit = { ...requestInit, headers };
|
|
2541
|
-
const response = await this.merchantFetch(input.url, requestInit);
|
|
2542
|
-
const surfaced = mcpSessionId ? await this.surfaceMcpResult(response) : response;
|
|
3504
|
+
let requestInit = withX402Wallet(input.init, x402Wallet) ?? {};
|
|
3505
|
+
if (mcpSessionId) requestInit = this.merchantTransport.withSessionHeaders(requestInit, mcpSessionId);
|
|
3506
|
+
const response = await this.merchantTransport.deliverPayment(input.url, requestInit, input.paymentHeader);
|
|
3507
|
+
const surfaced = mcpSessionId ? await this.merchantTransport.surfaceResult(response) : response;
|
|
2543
3508
|
const protocolReceiptHeader = surfaced.headers.get("PAYMENT-RESPONSE") ?? void 0;
|
|
2544
3509
|
const settlement = parseMerchantSettlement(protocolReceiptHeader ?? null);
|
|
2545
3510
|
const text = await surfaced.text();
|
|
@@ -2551,7 +3516,7 @@ var HavenClient = class {
|
|
|
2551
3516
|
}
|
|
2552
3517
|
if (!surfaced.ok) {
|
|
2553
3518
|
if (!input.noFundingLeg && fundingTxHash) {
|
|
2554
|
-
await this.
|
|
3519
|
+
await this.merchantCompletion.recordRetryRejected({
|
|
2555
3520
|
rail: "x402",
|
|
2556
3521
|
paymentId: evidenceContext.paymentId,
|
|
2557
3522
|
txHash: fundingTxHash,
|
|
@@ -2569,7 +3534,7 @@ var HavenClient = class {
|
|
|
2569
3534
|
}
|
|
2570
3535
|
} else {
|
|
2571
3536
|
if (!input.noFundingLeg && fundingTxHash) {
|
|
2572
|
-
await this.
|
|
3537
|
+
await this.merchantCompletion.reportEvidence({
|
|
2573
3538
|
paymentId: evidenceContext.paymentId,
|
|
2574
3539
|
rail: "x402",
|
|
2575
3540
|
txHash: fundingTxHash,
|
|
@@ -2581,7 +3546,7 @@ var HavenClient = class {
|
|
|
2581
3546
|
protocolReceiptHeader
|
|
2582
3547
|
});
|
|
2583
3548
|
}
|
|
2584
|
-
await this.reportMerchantReceipt(evidenceContext.paymentId, surfaced);
|
|
3549
|
+
await this.merchantCompletion.reportMerchantReceipt(evidenceContext.paymentId, surfaced);
|
|
2585
3550
|
}
|
|
2586
3551
|
return {
|
|
2587
3552
|
status: surfaced.status,
|
|
@@ -2611,271 +3576,11 @@ var HavenClient = class {
|
|
|
2611
3576
|
arguments: raw.arguments ?? {},
|
|
2612
3577
|
...raw.mcp_transport ? {
|
|
2613
3578
|
mcpTransport: {
|
|
2614
|
-
handshakeRequired: raw.mcp_transport.handshake_required,
|
|
2615
|
-
source: raw.mcp_transport.source
|
|
2616
|
-
}
|
|
2617
|
-
} : {}
|
|
2618
|
-
};
|
|
2619
|
-
}
|
|
2620
|
-
async resolveX402MerchantCompletionContext(input) {
|
|
2621
|
-
const status = await this.getPaymentStatus(input.paymentId);
|
|
2622
|
-
if (status.rail !== "x402") {
|
|
2623
|
-
throw new HavenPaymentStateError(
|
|
2624
|
-
`Payment ${status.paymentId} is ${status.rail}, not x402.`,
|
|
2625
|
-
409,
|
|
2626
|
-
status
|
|
2627
|
-
);
|
|
2628
|
-
}
|
|
2629
|
-
const readyForMerchantCompletion = input.noFundingLeg ? status.kind === "payment_intent" && status.status === "submitted" : status.nextAction === AgentPaymentNextAction.RetryOriginalX402Request || status.kind === "payment_intent" && status.status === "confirmed" && status.phase === AgentPaymentPhase.PaymentConfirmed && status.nextAction === AgentPaymentNextAction.None;
|
|
2630
|
-
if (!readyForMerchantCompletion) {
|
|
2631
|
-
throw new HavenPaymentStateError(status.message, PAYMENT_STATE_STATUS_CODES[status.status] ?? 409, status);
|
|
2632
|
-
}
|
|
2633
|
-
if (!input.noFundingLeg && !status.txHash) {
|
|
2634
|
-
throw new HavenApiError(
|
|
2635
|
-
`x402 payment ${status.paymentId} is ready for merchant completion but has no Haven transaction hash.`,
|
|
2636
|
-
502,
|
|
2637
|
-
status,
|
|
2638
|
-
status.paymentId
|
|
2639
|
-
);
|
|
2640
|
-
}
|
|
2641
|
-
const approvedResourceUrl = status.resourceUrl ?? status.x402?.resourceUrl ?? null;
|
|
2642
|
-
if (approvedResourceUrl && approvedResourceUrl !== input.url) {
|
|
2643
|
-
throw new HavenApiError(
|
|
2644
|
-
"x402 merchant completion does not match the approved resource URL.",
|
|
2645
|
-
409,
|
|
2646
|
-
{ status, url: input.url },
|
|
2647
|
-
status.paymentId
|
|
2648
|
-
);
|
|
2649
|
-
}
|
|
2650
|
-
return {
|
|
2651
|
-
paymentId: status.paymentId,
|
|
2652
|
-
txHash: status.txHash,
|
|
2653
|
-
resourceUrl: approvedResourceUrl ?? input.url,
|
|
2654
|
-
merchantAddress: status.merchantAddress ?? status.x402?.merchantAddress ?? null
|
|
2655
|
-
};
|
|
2656
|
-
}
|
|
2657
|
-
async resolveX402WalletForMerchantCall() {
|
|
2658
|
-
const localWallet = this.x402PayerAddress();
|
|
2659
|
-
if (localWallet) return localWallet;
|
|
2660
|
-
try {
|
|
2661
|
-
const agent = await this.getAgent();
|
|
2662
|
-
return agent.delegateAddress ?? void 0;
|
|
2663
|
-
} catch {
|
|
2664
|
-
return void 0;
|
|
2665
|
-
}
|
|
2666
|
-
}
|
|
2667
|
-
// #1328: authorizeMachinePayment / authorizeMppDemoPayment / resumeAuthorizedMpp
|
|
2668
|
-
// / resumeMppPayment / fetchWithMachinePayment / retryMppRequest (the
|
|
2669
|
-
// MACHINE-PAYMENT-CHALLENGE / mpp_demo client surface) are retired — the
|
|
2670
|
-
// backend's POST /machine-payments/authorize refuses unconditionally now,
|
|
2671
|
-
// and MACHINE-PAYMENT-CHALLENGE was never produced by any other Haven
|
|
2672
|
-
// surface. Use the x402 flow (authorizeX402 / fetch / quoteX402 / payX402Quote)
|
|
2673
|
-
// for agent-to-merchant payments.
|
|
2674
|
-
assertCanResumeX402(status, paymentRequired, option) {
|
|
2675
|
-
if (status.rail !== "x402") {
|
|
2676
|
-
throw new HavenPaymentStateError(
|
|
2677
|
-
`Payment ${status.paymentId} is ${status.rail}, not x402.`,
|
|
2678
|
-
409,
|
|
2679
|
-
status
|
|
2680
|
-
);
|
|
2681
|
-
}
|
|
2682
|
-
if (status.nextAction !== AgentPaymentNextAction.RetryOriginalX402Request) {
|
|
2683
|
-
throw new HavenPaymentStateError(status.message, PAYMENT_STATE_STATUS_CODES[status.status] ?? 409, status);
|
|
2684
|
-
}
|
|
2685
|
-
if (!status.txHash) {
|
|
2686
|
-
throw new HavenApiError(
|
|
2687
|
-
`x402 payment ${status.paymentId} is ready to retry but has no Haven transaction hash.`,
|
|
2688
|
-
502,
|
|
2689
|
-
status,
|
|
2690
|
-
status.paymentId
|
|
2691
|
-
);
|
|
2692
|
-
}
|
|
2693
|
-
if (status.resourceUrl && status.resourceUrl !== paymentRequired.resource.url) {
|
|
2694
|
-
throw new HavenApiError(
|
|
2695
|
-
"x402 resume request does not match the approved resource URL.",
|
|
2696
|
-
409,
|
|
2697
|
-
{ status, paymentRequired },
|
|
2698
|
-
status.paymentId
|
|
2699
|
-
);
|
|
2700
|
-
}
|
|
2701
|
-
if (status.merchantAddress && !sameAddress3(status.merchantAddress, option.payTo)) {
|
|
2702
|
-
throw new HavenApiError(
|
|
2703
|
-
"x402 resume request does not match the approved merchant.",
|
|
2704
|
-
409,
|
|
2705
|
-
{ status, selectedPayment: option },
|
|
2706
|
-
status.paymentId
|
|
2707
|
-
);
|
|
2708
|
-
}
|
|
2709
|
-
const optionChainId = chainIdFromNetwork(option.network);
|
|
2710
|
-
if (status.chainId && optionChainId && status.chainId !== optionChainId) {
|
|
2711
|
-
throw new HavenApiError(
|
|
2712
|
-
"x402 resume request does not match the approved network.",
|
|
2713
|
-
409,
|
|
2714
|
-
{ status, selectedPayment: option },
|
|
2715
|
-
status.paymentId
|
|
2716
|
-
);
|
|
2717
|
-
}
|
|
2718
|
-
if (status.token && status.token !== "USDC") {
|
|
2719
|
-
throw new HavenApiError(
|
|
2720
|
-
"x402 resume request does not match the approved token.",
|
|
2721
|
-
409,
|
|
2722
|
-
{ status, selectedPayment: option },
|
|
2723
|
-
status.paymentId
|
|
2724
|
-
);
|
|
2725
|
-
}
|
|
2726
|
-
const approvedAmount = status.amount ? normalizeDecimal(status.amount) : "";
|
|
2727
|
-
const requestedAmount = normalizeDecimal(decimalFromUsdcAtomic(x402AuthorizationAmount(option)));
|
|
2728
|
-
if (approvedAmount && approvedAmount !== requestedAmount) {
|
|
2729
|
-
throw new HavenApiError(
|
|
2730
|
-
"x402 resume request does not match the approved amount.",
|
|
2731
|
-
409,
|
|
2732
|
-
{ status, selectedPayment: option },
|
|
2733
|
-
status.paymentId
|
|
2734
|
-
);
|
|
2735
|
-
}
|
|
2736
|
-
}
|
|
2737
|
-
mapX402ReceiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult) {
|
|
2738
|
-
const txHash = execResult?.tx_hash ?? raw.tx_hash ?? "";
|
|
2739
|
-
const chainId = execResult?.chain_id ?? raw.chain_id ?? chainIdFromNetwork(option.network);
|
|
2740
|
-
const token = execResult?.token ?? raw.token ?? "USDC";
|
|
2741
|
-
const amount = execResult?.amount ?? raw.amount ?? decimalFromUsdcAtomic(x402AuthorizationAmount(option));
|
|
2742
|
-
const to = execResult?.to ?? raw.to ?? this.delegateAddress ?? "";
|
|
2743
|
-
const explorerUrl = execResult?.explorer_url ?? raw.explorer_url ?? explorerUrlOrEmpty(chainId, txHash);
|
|
2744
|
-
const merchantTo = execResult?.merchant_to ?? raw.merchant_to ?? option.payTo;
|
|
2745
|
-
const payer = raw.payer ?? raw.safe_address ?? raw.sign_data?.components.safe;
|
|
2746
|
-
return this.buildX402Receipt({
|
|
2747
|
-
paymentId: raw.payment_id,
|
|
2748
|
-
txHash,
|
|
2749
|
-
token,
|
|
2750
|
-
amount,
|
|
2751
|
-
to,
|
|
2752
|
-
resourceUrl: paymentRequired.resource.url,
|
|
2753
|
-
explorerUrl,
|
|
2754
|
-
accepted: option,
|
|
2755
|
-
paymentHeader,
|
|
2756
|
-
merchantTo,
|
|
2757
|
-
payer,
|
|
2758
|
-
chainId
|
|
2759
|
-
});
|
|
2760
|
-
}
|
|
2761
|
-
mapX402ReceiptFromStatus(paymentRequired, option, paymentHeader, status) {
|
|
2762
|
-
if (!status.txHash) {
|
|
2763
|
-
throw new HavenApiError(
|
|
2764
|
-
`x402 payment ${status.paymentId} is ready to retry but has no Haven transaction hash.`,
|
|
2765
|
-
502,
|
|
2766
|
-
status,
|
|
2767
|
-
status.paymentId
|
|
2768
|
-
);
|
|
2769
|
-
}
|
|
2770
|
-
return this.buildX402Receipt({
|
|
2771
|
-
paymentId: status.paymentId,
|
|
2772
|
-
txHash: status.txHash,
|
|
2773
|
-
token: status.token || "USDC",
|
|
2774
|
-
amount: status.amount || decimalFromUsdcAtomic(x402AuthorizationAmount(option)),
|
|
2775
|
-
to: this.delegateAddress ?? "",
|
|
2776
|
-
resourceUrl: paymentRequired.resource.url,
|
|
2777
|
-
explorerUrl: explorerUrlOrEmpty(status.chainId, status.txHash),
|
|
2778
|
-
accepted: option,
|
|
2779
|
-
paymentHeader,
|
|
2780
|
-
merchantTo: status.merchantAddress ?? option.payTo,
|
|
2781
|
-
payer: this.x402Wallet,
|
|
2782
|
-
chainId: status.chainId || chainIdFromNetwork(option.network)
|
|
2783
|
-
});
|
|
2784
|
-
}
|
|
2785
|
-
buildX402Receipt(input) {
|
|
2786
|
-
const fundingExplorerUrl = input.explorerUrl || explorerUrlOrEmpty(input.chainId, input.txHash);
|
|
2787
|
-
return {
|
|
2788
|
-
success: true,
|
|
2789
|
-
paymentId: input.paymentId,
|
|
2790
|
-
txHash: input.txHash,
|
|
2791
|
-
token: input.token,
|
|
2792
|
-
amount: input.amount,
|
|
2793
|
-
to: input.to,
|
|
2794
|
-
resourceUrl: input.resourceUrl,
|
|
2795
|
-
explorerUrl: input.explorerUrl,
|
|
2796
|
-
accepted: input.accepted,
|
|
2797
|
-
paymentHeader: input.paymentHeader,
|
|
2798
|
-
merchantTo: input.merchantTo ?? input.accepted.payTo,
|
|
2799
|
-
payer: input.payer,
|
|
2800
|
-
chainId: input.chainId,
|
|
2801
|
-
haven: {
|
|
2802
|
-
paymentId: input.paymentId,
|
|
2803
|
-
fundingTxHash: input.txHash,
|
|
2804
|
-
fundingExplorerUrl
|
|
2805
|
-
},
|
|
2806
|
-
merchant: {
|
|
2807
|
-
payTo: input.merchantTo ?? input.accepted.payTo
|
|
2808
|
-
},
|
|
2809
|
-
x402: {
|
|
2810
|
-
amount: x402AuthorizationAmount(input.accepted),
|
|
2811
|
-
token: input.token,
|
|
2812
|
-
network: input.accepted.network,
|
|
2813
|
-
asset: input.accepted.asset,
|
|
2814
|
-
resource: input.accepted.resource ?? input.resourceUrl
|
|
2815
|
-
}
|
|
2816
|
-
};
|
|
2817
|
-
}
|
|
2818
|
-
async createStandardX402Header(paymentRequired, option) {
|
|
2819
|
-
if (!this.delegateKey) {
|
|
2820
|
-
throw new HavenSigningError("delegateKey is required to sign x402 payment headers.");
|
|
2821
|
-
}
|
|
2822
|
-
const account = privateKeyToAccount(this.delegateKey);
|
|
2823
|
-
const requirements = toStandardPaymentRequirements(paymentRequired, option);
|
|
2824
|
-
const header = await exact.evm.createPaymentHeader(
|
|
2825
|
-
account,
|
|
2826
|
-
paymentRequired.x402Version,
|
|
2827
|
-
requirements
|
|
2828
|
-
);
|
|
2829
|
-
if (paymentRequired.x402Version < 2) return header;
|
|
2830
|
-
const payment = decodeBase64Json(header);
|
|
2831
|
-
return encodeBase64Json({
|
|
2832
|
-
x402Version: paymentRequired.x402Version,
|
|
2833
|
-
accepted: option,
|
|
2834
|
-
payload: payment.payload
|
|
2835
|
-
});
|
|
2836
|
-
}
|
|
2837
|
-
cacheX402Receipt(idempotencyKey, paymentHeader, receipt) {
|
|
2838
|
-
const expiresAt = getPaymentHeaderValidBefore(paymentHeader);
|
|
2839
|
-
if (expiresAt > Date.now()) {
|
|
2840
|
-
this.x402ReceiptCache.set(idempotencyKey, { expiresAt, receipt });
|
|
2841
|
-
}
|
|
2842
|
-
}
|
|
2843
|
-
async recordMerchantRetryRejected(input) {
|
|
2844
|
-
try {
|
|
2845
|
-
await this.post("/machine-payments/reconciliation-events", {
|
|
2846
|
-
paymentId: input.paymentId,
|
|
2847
|
-
rail: input.rail,
|
|
2848
|
-
eventType: "merchant_retry_rejected_after_payment",
|
|
2849
|
-
txHash: input.txHash,
|
|
2850
|
-
reason: `Merchant returned HTTP ${input.merchant.merchant_status} after Haven payment confirmation`,
|
|
2851
|
-
details: {
|
|
2852
|
-
resource_url: input.resourceUrl,
|
|
2853
|
-
retry_status: input.merchant.merchant_status,
|
|
2854
|
-
retry_body: input.merchant.merchant_body.slice(0, MERCHANT_BODY_SNIPPET_LIMIT) || null,
|
|
2855
|
-
...input.details
|
|
2856
|
-
}
|
|
2857
|
-
});
|
|
2858
|
-
} catch {
|
|
2859
|
-
}
|
|
2860
|
-
}
|
|
2861
|
-
async reportMachinePaymentEvidence(input) {
|
|
2862
|
-
try {
|
|
2863
|
-
await this.post("/machine-payments/evidence", {
|
|
2864
|
-
paymentId: input.paymentId,
|
|
2865
|
-
rail: input.rail,
|
|
2866
|
-
txHash: input.txHash,
|
|
2867
|
-
resourceUrl: input.resourceUrl,
|
|
2868
|
-
merchantStatus: input.merchantStatus,
|
|
2869
|
-
challengePayload: input.challengePayload,
|
|
2870
|
-
selectedPayment: input.selectedPayment,
|
|
2871
|
-
paymentProofHeaderName: input.paymentProofHeaderName,
|
|
2872
|
-
paymentProofHeader: input.paymentProofHeader,
|
|
2873
|
-
protocolReceiptHeaderName: input.protocolReceiptHeaderName,
|
|
2874
|
-
protocolReceiptHeader: input.protocolReceiptHeader,
|
|
2875
|
-
protocolReceiptPayload: input.protocolReceiptHeader ? parseProtocolReceiptHeader(input.protocolReceiptHeader) : void 0
|
|
2876
|
-
});
|
|
2877
|
-
} catch {
|
|
2878
|
-
}
|
|
3579
|
+
handshakeRequired: raw.mcp_transport.handshake_required,
|
|
3580
|
+
source: raw.mcp_transport.source
|
|
3581
|
+
}
|
|
3582
|
+
} : {}
|
|
3583
|
+
};
|
|
2879
3584
|
}
|
|
2880
3585
|
/**
|
|
2881
3586
|
* Wait for a funding tx to be mined with ≥1 confirmation before the
|
|
@@ -2886,264 +3591,9 @@ var HavenClient = class {
|
|
|
2886
3591
|
* backend has already confirmed on-chain submission and callers accept the
|
|
2887
3592
|
* small propagation window as a trade-off for not configuring an RPC URL.
|
|
2888
3593
|
*/
|
|
2889
|
-
async waitForFundingTx(txHash, chainId, timeoutMs = 3e4) {
|
|
2890
|
-
if (!txHash || !chainId) return;
|
|
2891
|
-
const rpcUrl = this.chainRpcs[chainId];
|
|
2892
|
-
if (!rpcUrl) return;
|
|
2893
|
-
const provider = createJsonRpcProvider(rpcUrl);
|
|
2894
|
-
const onChainReceipt = await provider.waitForTransaction(txHash, 1, timeoutMs);
|
|
2895
|
-
if (!onChainReceipt || onChainReceipt.status !== 1) {
|
|
2896
|
-
throw new HavenApiError(
|
|
2897
|
-
"Funding tx did not confirm on-chain within the timeout window.",
|
|
2898
|
-
500,
|
|
2899
|
-
{ txHash, chainId }
|
|
2900
|
-
);
|
|
2901
|
-
}
|
|
2902
|
-
}
|
|
2903
|
-
/**
|
|
2904
|
-
* Can the delegate EOA still fund an authorization for `amountAtomic`?
|
|
2905
|
-
*
|
|
2906
|
-
* #1521: the only question that separates a legitimate resume (funding
|
|
2907
|
-
* confirmed, merchant never paid — the delegate still holds the money) from
|
|
2908
|
-
* a replayed settled payment (funding confirmed, merchant paid, delegate
|
|
2909
|
-
* spent). The intent's own `status: 'confirmed'` is identical in both.
|
|
2910
|
-
*
|
|
2911
|
-
* The balance is asked of the CHAIN rather than of Haven's bookkeeping on
|
|
2912
|
-
* purpose: the merchant-settlement evidence record is written by this SDK
|
|
2913
|
-
* *after* the merchant call, so a client that dies between the two leaves
|
|
2914
|
-
* the backend believing the merchant was never paid — the exact case the
|
|
2915
|
-
* discriminator has to get right. The chain cannot be behind in that way.
|
|
2916
|
-
*
|
|
2917
|
-
* Returns `null` — never a guess — when `chainRpcs` has no entry for the
|
|
2918
|
-
* chain or the read fails. Callers must treat that as "unverifiable", not
|
|
2919
|
-
* as "funded".
|
|
2920
|
-
*/
|
|
2921
|
-
async delegateCanFund(chainId, tokenAddress, amountAtomic, timeoutMs = 1e4) {
|
|
2922
|
-
if (!chainId || !this.delegateAddress) return null;
|
|
2923
|
-
const rpcUrl = this.chainRpcs[chainId];
|
|
2924
|
-
if (!rpcUrl) return null;
|
|
2925
|
-
try {
|
|
2926
|
-
const provider = createJsonRpcProvider(rpcUrl);
|
|
2927
|
-
const token = createErc20Contract(
|
|
2928
|
-
tokenAddress,
|
|
2929
|
-
["function balanceOf(address) view returns (uint256)"],
|
|
2930
|
-
provider
|
|
2931
|
-
);
|
|
2932
|
-
const balance = await Promise.race([
|
|
2933
|
-
token.balanceOf(this.delegateAddress),
|
|
2934
|
-
new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs).unref?.())
|
|
2935
|
-
]);
|
|
2936
|
-
if (balance === null) return null;
|
|
2937
|
-
return balance >= BigInt(amountAtomic);
|
|
2938
|
-
} catch {
|
|
2939
|
-
return null;
|
|
2940
|
-
}
|
|
2941
|
-
}
|
|
2942
3594
|
throwIfNonSignableAuthorizationState(label, raw) {
|
|
2943
3595
|
if (raw.status === "pending_signature") return;
|
|
2944
|
-
|
|
2945
|
-
}
|
|
2946
|
-
throwPaymentStateError(label, raw) {
|
|
2947
|
-
const statusCode = PAYMENT_STATE_STATUS_CODES[raw.status] ?? 502;
|
|
2948
|
-
const state = this.paymentStateFromRaw(label, raw);
|
|
2949
|
-
if (state) {
|
|
2950
|
-
throw new HavenPaymentStateError(state.message, statusCode, state, raw);
|
|
2951
|
-
}
|
|
2952
|
-
if (raw.status === "pending_approval") {
|
|
2953
|
-
throw new HavenApiError(
|
|
2954
|
-
`${label} exceeds the on-chain allowance and was queued for owner approval (payment_id: ${raw.payment_id}).`,
|
|
2955
|
-
statusCode,
|
|
2956
|
-
raw
|
|
2957
|
-
);
|
|
2958
|
-
}
|
|
2959
|
-
if (raw.status === "expired") {
|
|
2960
|
-
throw new HavenApiError(
|
|
2961
|
-
`${label} expired before it could be completed (payment_id: ${raw.payment_id}).`,
|
|
2962
|
-
statusCode,
|
|
2963
|
-
raw
|
|
2964
|
-
);
|
|
2965
|
-
}
|
|
2966
|
-
const paymentId = raw.payment_id ? ` (payment_id: ${raw.payment_id})` : "";
|
|
2967
|
-
const message = raw.error ?? `${label} ${raw.status}${paymentId}`;
|
|
2968
|
-
throw new HavenApiError(message, statusCode, raw);
|
|
2969
|
-
}
|
|
2970
|
-
paymentStateFromRaw(label, raw) {
|
|
2971
|
-
if (!raw.payment_id || !raw.status) return null;
|
|
2972
|
-
const phase = raw.phase ?? phaseForStatus(raw.status);
|
|
2973
|
-
const nextAction = raw.next_action ?? nextActionForStatus(raw.status);
|
|
2974
|
-
if (!phase || !nextAction) return null;
|
|
2975
|
-
const amount = raw.amount ?? raw.requested ?? "";
|
|
2976
|
-
const token = raw.token ?? "";
|
|
2977
|
-
const message = raw.message ?? raw.error ?? messageForState(label, raw.status, raw.payment_id, nextAction);
|
|
2978
|
-
return {
|
|
2979
|
-
paymentId: raw.payment_id,
|
|
2980
|
-
kind: raw.kind === "payment_intent" ? "payment_intent" : "approval_request",
|
|
2981
|
-
rail: raw.rail ?? "direct",
|
|
2982
|
-
status: raw.status === "pending" ? "pending_approval" : raw.status,
|
|
2983
|
-
phase,
|
|
2984
|
-
nextAction,
|
|
2985
|
-
amount,
|
|
2986
|
-
token,
|
|
2987
|
-
resourceUrl: raw.resource_url ?? null,
|
|
2988
|
-
merchantAddress: raw.merchant_address ?? raw.merchant_to ?? null,
|
|
2989
|
-
txHash: raw.tx_hash ?? null,
|
|
2990
|
-
expiresAt: raw.expires_at ?? "",
|
|
2991
|
-
chainId: raw.chain_id ?? 0,
|
|
2992
|
-
message,
|
|
2993
|
-
amountAtomic: raw.amount_atomic ?? raw.x402?.amount_atomic ?? raw.mpp?.amount_atomic ?? null,
|
|
2994
|
-
asset: raw.asset ?? raw.x402?.asset ?? raw.mpp?.asset ?? null,
|
|
2995
|
-
network: raw.network ?? raw.x402?.network ?? raw.mpp?.network ?? null,
|
|
2996
|
-
description: raw.description ?? raw.x402?.description ?? raw.mpp?.description ?? null,
|
|
2997
|
-
idempotencyKey: raw.idempotency_key ?? raw.x402?.idempotency_key ?? raw.mpp?.idempotency_key ?? null,
|
|
2998
|
-
x402: raw.x402 ? {
|
|
2999
|
-
amountAtomic: raw.x402.amount_atomic ?? raw.amount_atomic ?? null,
|
|
3000
|
-
asset: raw.x402.asset ?? raw.asset ?? null,
|
|
3001
|
-
network: raw.x402.network ?? raw.network ?? null,
|
|
3002
|
-
resourceUrl: raw.x402.resource_url ?? raw.resource_url ?? null,
|
|
3003
|
-
merchantAddress: raw.x402.merchant_address ?? raw.merchant_address ?? raw.merchant_to ?? null,
|
|
3004
|
-
description: raw.x402.description ?? raw.description ?? null,
|
|
3005
|
-
idempotencyKey: raw.x402.idempotency_key ?? raw.idempotency_key ?? null
|
|
3006
|
-
} : void 0,
|
|
3007
|
-
mpp: raw.mpp ? {
|
|
3008
|
-
amountAtomic: raw.mpp.amount_atomic ?? raw.amount_atomic ?? null,
|
|
3009
|
-
asset: raw.mpp.asset ?? raw.asset ?? null,
|
|
3010
|
-
network: raw.mpp.network ?? raw.network ?? null,
|
|
3011
|
-
resourceUrl: raw.mpp.resource_url ?? raw.resource_url ?? null,
|
|
3012
|
-
merchantAddress: raw.mpp.merchant_address ?? raw.merchant_address ?? raw.merchant_to ?? null,
|
|
3013
|
-
description: raw.mpp.description ?? raw.description ?? null,
|
|
3014
|
-
idempotencyKey: raw.mpp.idempotency_key ?? raw.idempotency_key ?? null,
|
|
3015
|
-
challengeId: raw.mpp.challenge_id ?? raw.challenge_id ?? null
|
|
3016
|
-
} : void 0
|
|
3017
|
-
};
|
|
3018
|
-
}
|
|
3019
|
-
x402PayerAddress() {
|
|
3020
|
-
return this.delegateAddress ?? this.x402Wallet;
|
|
3021
|
-
}
|
|
3022
|
-
snapshotX402Request(url, init) {
|
|
3023
|
-
return {
|
|
3024
|
-
url,
|
|
3025
|
-
method: init?.method ?? "GET",
|
|
3026
|
-
headers: Array.from(new Headers(init?.headers).entries()),
|
|
3027
|
-
body: this.snapshotRequestBody(init?.body)
|
|
3028
|
-
};
|
|
3029
|
-
}
|
|
3030
|
-
snapshotRequestBody(body) {
|
|
3031
|
-
if (body == null) return void 0;
|
|
3032
|
-
if (typeof body === "string") return body;
|
|
3033
|
-
if (body instanceof URLSearchParams) return body.toString();
|
|
3034
|
-
throw new HavenApiError(
|
|
3035
|
-
"Quote helpers can only capture resumable request bodies that are strings or URLSearchParams. For streams, blobs, or binary bodies, preserve the original request yourself and call the matching resume method with fresh init.",
|
|
3036
|
-
400
|
|
3037
|
-
);
|
|
3038
|
-
}
|
|
3039
|
-
requestInitFromSnapshot(request) {
|
|
3040
|
-
return {
|
|
3041
|
-
method: request.method,
|
|
3042
|
-
headers: request.headers,
|
|
3043
|
-
body: request.body
|
|
3044
|
-
};
|
|
3045
|
-
}
|
|
3046
|
-
withX402Wallet(init, wallet = this.x402PayerAddress()) {
|
|
3047
|
-
if (!wallet) return init;
|
|
3048
|
-
const headers = new Headers(init?.headers);
|
|
3049
|
-
if (!headers.has("x402-wallet")) {
|
|
3050
|
-
headers.set("x402-wallet", wallet);
|
|
3051
|
-
}
|
|
3052
|
-
return {
|
|
3053
|
-
...init,
|
|
3054
|
-
headers
|
|
3055
|
-
};
|
|
3056
|
-
}
|
|
3057
|
-
buildX402Quote(paymentRequired, request, idempotencyKey, mcpTransport) {
|
|
3058
|
-
const option = selectStandardPaymentOption(paymentRequired.accepts);
|
|
3059
|
-
if (!option) {
|
|
3060
|
-
throw new HavenApiError(
|
|
3061
|
-
"No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC.",
|
|
3062
|
-
400
|
|
3063
|
-
);
|
|
3064
|
-
}
|
|
3065
|
-
const token = resolveTokenFromAddress(option.asset, option.network);
|
|
3066
|
-
return {
|
|
3067
|
-
rail: "x402",
|
|
3068
|
-
idempotencyKey: idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option),
|
|
3069
|
-
paymentRequired,
|
|
3070
|
-
accepted: option,
|
|
3071
|
-
request,
|
|
3072
|
-
...mcpTransport ? { mcpTransport } : {},
|
|
3073
|
-
resourceUrl: paymentRequired.resource.url,
|
|
3074
|
-
description: paymentRequired.resource.description ?? option.description ?? null,
|
|
3075
|
-
mimeType: paymentRequired.resource.mimeType ?? option.mimeType ?? null,
|
|
3076
|
-
amountAtomic: x402AuthorizationAmount(option),
|
|
3077
|
-
amount: decimalFromUsdcAtomic(x402AuthorizationAmount(option)),
|
|
3078
|
-
token: token?.symbol ?? "USDC",
|
|
3079
|
-
// #1351: null when the asset is unrecognised on this network — the
|
|
3080
|
-
// `token` fallback above is a LABEL, not evidence of 6 decimals, and a
|
|
3081
|
-
// human-denominated cap must fail closed rather than convert against a
|
|
3082
|
-
// guess. Same resolution as `token`, so the two never disagree.
|
|
3083
|
-
decimals: token?.decimals ?? null,
|
|
3084
|
-
asset: option.asset,
|
|
3085
|
-
network: option.network,
|
|
3086
|
-
chainId: chainIdOrNull(option.network),
|
|
3087
|
-
merchantAddress: option.payTo,
|
|
3088
|
-
maxTimeoutSeconds: option.maxTimeoutSeconds
|
|
3089
|
-
};
|
|
3090
|
-
}
|
|
3091
|
-
async detectX402McpTransport(url, paymentRequired, response) {
|
|
3092
|
-
if (isMcpUrl(url)) {
|
|
3093
|
-
return { handshakeRequired: true, source: "path" };
|
|
3094
|
-
}
|
|
3095
|
-
if (paymentRequired.extensions?.bazaar != null) {
|
|
3096
|
-
return { handshakeRequired: true, source: "bazaar" };
|
|
3097
|
-
}
|
|
3098
|
-
if (await responseHasBazaarExtension(response)) {
|
|
3099
|
-
return { handshakeRequired: true, source: "bazaar" };
|
|
3100
|
-
}
|
|
3101
|
-
return void 0;
|
|
3102
|
-
}
|
|
3103
|
-
buildX402ResumeState(input) {
|
|
3104
|
-
const token = resolveTokenFromAddress(input.accepted.asset, input.accepted.network);
|
|
3105
|
-
return {
|
|
3106
|
-
rail: "x402",
|
|
3107
|
-
paymentId: input.paymentId,
|
|
3108
|
-
idempotencyKey: input.idempotencyKey,
|
|
3109
|
-
paymentRequired: input.paymentRequired,
|
|
3110
|
-
accepted: input.accepted,
|
|
3111
|
-
url: input.request?.url ?? input.paymentRequired.resource.url,
|
|
3112
|
-
request: input.request,
|
|
3113
|
-
resourceUrl: input.paymentRequired.resource.url,
|
|
3114
|
-
description: input.paymentRequired.resource.description ?? input.accepted.description ?? null,
|
|
3115
|
-
amountAtomic: x402AuthorizationAmount(input.accepted),
|
|
3116
|
-
amount: decimalFromUsdcAtomic(x402AuthorizationAmount(input.accepted)),
|
|
3117
|
-
token: token?.symbol ?? "USDC",
|
|
3118
|
-
asset: input.accepted.asset,
|
|
3119
|
-
network: input.accepted.network,
|
|
3120
|
-
chainId: chainIdOrNull(input.accepted.network),
|
|
3121
|
-
merchantAddress: input.accepted.payTo
|
|
3122
|
-
};
|
|
3123
|
-
}
|
|
3124
|
-
// #1328: attachResumeState's 'mpp' branch (buildMppQuote / buildMppResumeState
|
|
3125
|
-
// / attachMppResumeState) is retired along with the rest of the MPP-demo
|
|
3126
|
-
// client surface — every remaining caller passes rail: 'x402' only, so this
|
|
3127
|
-
// is now a direct alias for attachX402ResumeState rather than a dispatcher.
|
|
3128
|
-
attachResumeState(err, input) {
|
|
3129
|
-
this.attachX402ResumeState(
|
|
3130
|
-
err,
|
|
3131
|
-
input.paymentRequired,
|
|
3132
|
-
input.accepted,
|
|
3133
|
-
input.idempotencyKey,
|
|
3134
|
-
input.request
|
|
3135
|
-
);
|
|
3136
|
-
}
|
|
3137
|
-
attachX402ResumeState(err, paymentRequired, accepted, idempotencyKey, request) {
|
|
3138
|
-
if (!(err instanceof HavenPaymentStateError)) return;
|
|
3139
|
-
if (err.state.rail !== "x402") return;
|
|
3140
|
-
err.resumeState = this.buildX402ResumeState({
|
|
3141
|
-
paymentId: err.state.paymentId,
|
|
3142
|
-
paymentRequired,
|
|
3143
|
-
accepted,
|
|
3144
|
-
idempotencyKey,
|
|
3145
|
-
request
|
|
3146
|
-
});
|
|
3596
|
+
throwPaymentStateError(label, raw);
|
|
3147
3597
|
}
|
|
3148
3598
|
// ── Tool Execution (for agent frameworks) ────────────────────────
|
|
3149
3599
|
/**
|
|
@@ -3175,19 +3625,19 @@ var HavenClient = class {
|
|
|
3175
3625
|
error: result.errorMessage
|
|
3176
3626
|
};
|
|
3177
3627
|
} catch (err) {
|
|
3178
|
-
return
|
|
3628
|
+
return toolError(err);
|
|
3179
3629
|
}
|
|
3180
3630
|
}
|
|
3181
3631
|
if (toolName === "authorize_x402_payment") {
|
|
3182
3632
|
const { url, payTo, amount, asset, network, description, idempotencyKey } = input;
|
|
3183
3633
|
try {
|
|
3184
3634
|
const receipt = await this.authorizeX402(
|
|
3185
|
-
|
|
3635
|
+
toolX402PaymentRequired({ url, payTo, amount, asset, network, description }),
|
|
3186
3636
|
{ idempotencyKey }
|
|
3187
3637
|
);
|
|
3188
|
-
return
|
|
3638
|
+
return x402ToolReceipt(receipt);
|
|
3189
3639
|
} catch (err) {
|
|
3190
|
-
return
|
|
3640
|
+
return toolError(err);
|
|
3191
3641
|
}
|
|
3192
3642
|
}
|
|
3193
3643
|
if (toolName === "resume_x402_payment") {
|
|
@@ -3195,12 +3645,12 @@ var HavenClient = class {
|
|
|
3195
3645
|
try {
|
|
3196
3646
|
const receipt = await this.resumeAuthorizedX402({
|
|
3197
3647
|
paymentId: payment_id,
|
|
3198
|
-
paymentRequired:
|
|
3648
|
+
paymentRequired: toolX402PaymentRequired({ url, payTo, amount, asset, network, description }),
|
|
3199
3649
|
idempotencyKey
|
|
3200
3650
|
});
|
|
3201
|
-
return
|
|
3651
|
+
return x402ToolReceipt(receipt);
|
|
3202
3652
|
} catch (err) {
|
|
3203
|
-
return
|
|
3653
|
+
return toolError(err);
|
|
3204
3654
|
}
|
|
3205
3655
|
}
|
|
3206
3656
|
if (toolName === "get_payment_status") {
|
|
@@ -3235,302 +3685,17 @@ var HavenClient = class {
|
|
|
3235
3685
|
}
|
|
3236
3686
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
3237
3687
|
}
|
|
3238
|
-
toolX402PaymentRequired(input) {
|
|
3239
|
-
return {
|
|
3240
|
-
x402Version: 2,
|
|
3241
|
-
resource: { url: input.url, description: input.description },
|
|
3242
|
-
accepts: [
|
|
3243
|
-
{
|
|
3244
|
-
scheme: "exact",
|
|
3245
|
-
network: input.network,
|
|
3246
|
-
amount: input.amount,
|
|
3247
|
-
asset: input.asset,
|
|
3248
|
-
payTo: input.payTo,
|
|
3249
|
-
maxTimeoutSeconds: 30
|
|
3250
|
-
}
|
|
3251
|
-
]
|
|
3252
|
-
};
|
|
3253
|
-
}
|
|
3254
|
-
x402ToolReceipt(receipt) {
|
|
3255
|
-
return {
|
|
3256
|
-
success: true,
|
|
3257
|
-
payment_id: receipt.paymentId,
|
|
3258
|
-
tx_hash: receipt.txHash,
|
|
3259
|
-
token: receipt.token,
|
|
3260
|
-
amount: receipt.amount,
|
|
3261
|
-
to: receipt.to,
|
|
3262
|
-
resource_url: receipt.resourceUrl,
|
|
3263
|
-
explorer_url: receipt.explorerUrl,
|
|
3264
|
-
payment_header: receipt.paymentHeader,
|
|
3265
|
-
merchant_to: receipt.merchantTo,
|
|
3266
|
-
payer: receipt.payer,
|
|
3267
|
-
chain_id: receipt.chainId,
|
|
3268
|
-
haven: receipt.haven,
|
|
3269
|
-
merchant: receipt.merchant,
|
|
3270
|
-
x402: receipt.x402
|
|
3271
|
-
};
|
|
3272
|
-
}
|
|
3273
|
-
toolError(err) {
|
|
3274
|
-
if (err instanceof HavenPaymentStateError) {
|
|
3275
|
-
return {
|
|
3276
|
-
success: false,
|
|
3277
|
-
payment_id: err.state.paymentId,
|
|
3278
|
-
kind: err.state.kind,
|
|
3279
|
-
rail: err.state.rail,
|
|
3280
|
-
status: err.state.status,
|
|
3281
|
-
phase: err.state.phase,
|
|
3282
|
-
next_action: err.state.nextAction,
|
|
3283
|
-
tx_hash: err.state.txHash,
|
|
3284
|
-
token: err.state.token,
|
|
3285
|
-
amount: err.state.amount,
|
|
3286
|
-
resource_url: err.state.resourceUrl,
|
|
3287
|
-
merchant_address: err.state.merchantAddress,
|
|
3288
|
-
amount_atomic: err.state.amountAtomic,
|
|
3289
|
-
asset: err.state.asset,
|
|
3290
|
-
network: err.state.network,
|
|
3291
|
-
description: err.state.description,
|
|
3292
|
-
idempotency_key: err.state.idempotencyKey,
|
|
3293
|
-
x402: err.state.x402 ? {
|
|
3294
|
-
amount_atomic: err.state.x402.amountAtomic,
|
|
3295
|
-
asset: err.state.x402.asset,
|
|
3296
|
-
network: err.state.x402.network,
|
|
3297
|
-
resource_url: err.state.x402.resourceUrl,
|
|
3298
|
-
merchant_address: err.state.x402.merchantAddress,
|
|
3299
|
-
description: err.state.x402.description,
|
|
3300
|
-
idempotency_key: err.state.x402.idempotencyKey
|
|
3301
|
-
} : void 0,
|
|
3302
|
-
mpp: err.state.mpp ? {
|
|
3303
|
-
amount_atomic: err.state.mpp.amountAtomic,
|
|
3304
|
-
asset: err.state.mpp.asset,
|
|
3305
|
-
network: err.state.mpp.network,
|
|
3306
|
-
resource_url: err.state.mpp.resourceUrl,
|
|
3307
|
-
merchant_address: err.state.mpp.merchantAddress,
|
|
3308
|
-
description: err.state.mpp.description,
|
|
3309
|
-
idempotency_key: err.state.mpp.idempotencyKey,
|
|
3310
|
-
challenge_id: err.state.mpp.challengeId
|
|
3311
|
-
} : void 0,
|
|
3312
|
-
resume_state: err.resumeState,
|
|
3313
|
-
expires_at: err.state.expiresAt,
|
|
3314
|
-
chain_id: err.state.chainId,
|
|
3315
|
-
message: err.state.message,
|
|
3316
|
-
error: err.message
|
|
3317
|
-
};
|
|
3318
|
-
}
|
|
3319
|
-
if (err instanceof HavenApiError) {
|
|
3320
|
-
return {
|
|
3321
|
-
success: false,
|
|
3322
|
-
status_code: err.statusCode,
|
|
3323
|
-
error: err.message,
|
|
3324
|
-
body: err.body
|
|
3325
|
-
};
|
|
3326
|
-
}
|
|
3327
|
-
return {
|
|
3328
|
-
success: false,
|
|
3329
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3330
|
-
};
|
|
3331
|
-
}
|
|
3332
3688
|
// ── HTTP Helpers ─────────────────────────────────────────────────
|
|
3333
3689
|
async post(path, body) {
|
|
3334
|
-
return this.
|
|
3690
|
+
return this.havenApi.post(path, body);
|
|
3335
3691
|
}
|
|
3336
3692
|
async get(path) {
|
|
3337
|
-
return this.
|
|
3338
|
-
}
|
|
3339
|
-
/**
|
|
3340
|
-
* #1300: every MERCHANT-facing fetch goes through here. Haven API calls
|
|
3341
|
-
* have always been bounded (request() below); the merchant probes/retries
|
|
3342
|
-
* called globalThis.fetch bare, so a slow-loris merchant could hold a tool
|
|
3343
|
-
* call open forever. A caller-supplied signal still applies (combined via
|
|
3344
|
-
* AbortSignal.any); a timeout abort surfaces as a clear HavenApiError 504
|
|
3345
|
-
* naming the URL rather than a bare AbortError.
|
|
3346
|
-
*/
|
|
3347
|
-
async merchantFetch(url, init = {}, timeoutMs = this.merchantTimeout) {
|
|
3348
|
-
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
3349
|
-
const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
|
|
3350
|
-
try {
|
|
3351
|
-
return await globalThis.fetch(url, { ...init, signal });
|
|
3352
|
-
} catch (err) {
|
|
3353
|
-
if (timeoutSignal.aborted) {
|
|
3354
|
-
throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${url}`);
|
|
3355
|
-
}
|
|
3356
|
-
throw err;
|
|
3357
|
-
}
|
|
3358
|
-
}
|
|
3359
|
-
async request(method, path, body) {
|
|
3360
|
-
const url = `${this.baseUrl}${path}`;
|
|
3361
|
-
const controller = new AbortController();
|
|
3362
|
-
const timeout = setTimeout(() => controller.abort(), this.requestTimeout);
|
|
3363
|
-
try {
|
|
3364
|
-
const contextHeaders = this.requestContext.getStore()?.headers ?? {};
|
|
3365
|
-
const res = await fetch(url, {
|
|
3366
|
-
method,
|
|
3367
|
-
headers: {
|
|
3368
|
-
"Content-Type": "application/json",
|
|
3369
|
-
"Authorization": `Bearer ${this.apiKey}`,
|
|
3370
|
-
...this.defaultHeaders,
|
|
3371
|
-
...contextHeaders
|
|
3372
|
-
},
|
|
3373
|
-
body: body ? JSON.stringify(body) : void 0,
|
|
3374
|
-
signal: controller.signal
|
|
3375
|
-
});
|
|
3376
|
-
const data = await res.json();
|
|
3377
|
-
if (!res.ok) {
|
|
3378
|
-
const record = data;
|
|
3379
|
-
const errorText = typeof record.error === "string" ? record.error : void 0;
|
|
3380
|
-
const rawDetails = record.details ?? record.detail;
|
|
3381
|
-
const detailsText = typeof rawDetails === "string" ? rawDetails : rawDetails != null ? JSON.stringify(rawDetails) : void 0;
|
|
3382
|
-
const message = errorText && detailsText ? `${errorText}: ${detailsText}` : errorText ?? detailsText ?? `API request failed`;
|
|
3383
|
-
throw new HavenApiError(message, res.status, data);
|
|
3384
|
-
}
|
|
3385
|
-
return data;
|
|
3386
|
-
} catch (err) {
|
|
3387
|
-
if (err instanceof HavenApiError) throw err;
|
|
3388
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
3389
|
-
throw new HavenApiError(`Request to ${path} timed out`, 408);
|
|
3390
|
-
}
|
|
3391
|
-
throw new HavenApiError(
|
|
3392
|
-
`Request to ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
3393
|
-
0
|
|
3394
|
-
);
|
|
3395
|
-
} finally {
|
|
3396
|
-
clearTimeout(timeout);
|
|
3397
|
-
}
|
|
3398
|
-
}
|
|
3399
|
-
// ── Mapping Helpers ──────────────────────────────────────────────
|
|
3400
|
-
mapPaymentResult(raw) {
|
|
3401
|
-
return {
|
|
3402
|
-
paymentId: raw.payment_id,
|
|
3403
|
-
status: raw.status,
|
|
3404
|
-
token: raw.token,
|
|
3405
|
-
amount: raw.amount,
|
|
3406
|
-
to: raw.to,
|
|
3407
|
-
txHash: raw.tx_hash,
|
|
3408
|
-
errorMessage: raw.error_message,
|
|
3409
|
-
explorerUrl: raw.explorer_url ?? (raw.tx_hash ? buildExplorerUrl(raw.chain_id, raw.tx_hash) : null),
|
|
3410
|
-
fee: raw.fee ? {
|
|
3411
|
-
amount: raw.fee.amount,
|
|
3412
|
-
token: raw.fee.token,
|
|
3413
|
-
basisPoints: raw.fee.basis_points,
|
|
3414
|
-
applied: raw.fee.applied
|
|
3415
|
-
} : null,
|
|
3416
|
-
createdAt: raw.created_at,
|
|
3417
|
-
signedAt: raw.signed_at,
|
|
3418
|
-
submittedAt: raw.submitted_at,
|
|
3419
|
-
confirmedAt: raw.confirmed_at,
|
|
3420
|
-
expiresAt: raw.expires_at
|
|
3421
|
-
};
|
|
3422
|
-
}
|
|
3423
|
-
mapPaymentStatusResult(raw) {
|
|
3424
|
-
return {
|
|
3425
|
-
paymentId: raw.payment_id,
|
|
3426
|
-
kind: raw.kind,
|
|
3427
|
-
rail: raw.rail,
|
|
3428
|
-
status: raw.status,
|
|
3429
|
-
phase: raw.phase,
|
|
3430
|
-
nextAction: raw.next_action,
|
|
3431
|
-
amount: raw.amount,
|
|
3432
|
-
token: raw.token,
|
|
3433
|
-
resourceUrl: raw.resource_url,
|
|
3434
|
-
merchantAddress: raw.merchant_address,
|
|
3435
|
-
payerAddress: raw.payer_address ?? null,
|
|
3436
|
-
txHash: raw.tx_hash,
|
|
3437
|
-
expiresAt: raw.expires_at,
|
|
3438
|
-
chainId: raw.chain_id,
|
|
3439
|
-
message: raw.message,
|
|
3440
|
-
fee: raw.fee ? {
|
|
3441
|
-
amount: raw.fee.amount,
|
|
3442
|
-
token: raw.fee.token,
|
|
3443
|
-
basisPoints: raw.fee.basis_points,
|
|
3444
|
-
applied: raw.fee.applied
|
|
3445
|
-
} : null,
|
|
3446
|
-
amountAtomic: raw.amount_atomic ?? raw.x402?.amount_atomic ?? null,
|
|
3447
|
-
asset: raw.asset ?? raw.x402?.asset ?? null,
|
|
3448
|
-
network: raw.network ?? raw.x402?.network ?? null,
|
|
3449
|
-
description: raw.description ?? raw.x402?.description ?? null,
|
|
3450
|
-
idempotencyKey: raw.idempotency_key ?? raw.x402?.idempotency_key ?? null,
|
|
3451
|
-
x402: raw.x402 ? {
|
|
3452
|
-
amountAtomic: raw.x402.amount_atomic ?? raw.amount_atomic ?? null,
|
|
3453
|
-
asset: raw.x402.asset ?? raw.asset ?? null,
|
|
3454
|
-
network: raw.x402.network ?? raw.network ?? null,
|
|
3455
|
-
resourceUrl: raw.x402.resource_url ?? raw.resource_url,
|
|
3456
|
-
merchantAddress: raw.x402.merchant_address ?? raw.merchant_address,
|
|
3457
|
-
description: raw.x402.description ?? raw.description ?? null,
|
|
3458
|
-
idempotencyKey: raw.x402.idempotency_key ?? raw.idempotency_key ?? null
|
|
3459
|
-
} : void 0
|
|
3460
|
-
};
|
|
3461
|
-
}
|
|
3462
|
-
mapPaymentReceipt(raw) {
|
|
3463
|
-
const receipt = {
|
|
3464
|
-
id: raw.id,
|
|
3465
|
-
paymentId: raw.payment_id,
|
|
3466
|
-
rail: raw.rail,
|
|
3467
|
-
proofStatus: raw.proof_status,
|
|
3468
|
-
txHash: raw.tx_hash,
|
|
3469
|
-
chainId: raw.chain_id,
|
|
3470
|
-
resourceUrl: raw.resource_url,
|
|
3471
|
-
merchantAddress: raw.merchant_address,
|
|
3472
|
-
payerAddress: raw.payer_address,
|
|
3473
|
-
settlementAddress: raw.settlement_address,
|
|
3474
|
-
tokenSymbol: raw.token_symbol,
|
|
3475
|
-
tokenAddress: raw.token_address,
|
|
3476
|
-
amountRaw: raw.amount_raw,
|
|
3477
|
-
amount: raw.amount_human,
|
|
3478
|
-
challengeId: raw.challenge_id,
|
|
3479
|
-
idempotencyKey: raw.idempotency_key,
|
|
3480
|
-
challengePayload: raw.challenge_payload,
|
|
3481
|
-
selectedPayment: raw.selected_payment,
|
|
3482
|
-
paymentProofHeaderName: raw.payment_proof_header_name,
|
|
3483
|
-
protocolReceiptHeaderName: raw.protocol_receipt_header_name,
|
|
3484
|
-
protocolReceiptPayload: raw.protocol_receipt_payload,
|
|
3485
|
-
merchantStatus: raw.merchant_status,
|
|
3486
|
-
confirmedAt: raw.confirmed_at,
|
|
3487
|
-
createdAt: raw.created_at,
|
|
3488
|
-
updatedAt: raw.updated_at
|
|
3489
|
-
};
|
|
3490
|
-
if ("payment_intent_id" in raw) {
|
|
3491
|
-
receipt.paymentIntentId = raw.payment_intent_id ?? null;
|
|
3492
|
-
}
|
|
3493
|
-
if ("approval_request_id" in raw) {
|
|
3494
|
-
receipt.approvalRequestId = raw.approval_request_id ?? null;
|
|
3495
|
-
}
|
|
3496
|
-
return receipt;
|
|
3693
|
+
return this.havenApi.get(path);
|
|
3497
3694
|
}
|
|
3498
3695
|
};
|
|
3499
3696
|
function sleep(ms) {
|
|
3500
3697
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3501
3698
|
}
|
|
3502
|
-
function getPaymentHeaderValidBefore(paymentHeader) {
|
|
3503
|
-
try {
|
|
3504
|
-
const payment = decodeBase64Json(
|
|
3505
|
-
paymentHeader
|
|
3506
|
-
);
|
|
3507
|
-
const payload = payment.payload;
|
|
3508
|
-
const validBeforeSeconds = Number(payload.authorization?.validBefore);
|
|
3509
|
-
if (Number.isFinite(validBeforeSeconds)) return validBeforeSeconds * 1e3;
|
|
3510
|
-
} catch {
|
|
3511
|
-
}
|
|
3512
|
-
return 0;
|
|
3513
|
-
}
|
|
3514
|
-
function parseProtocolReceiptHeader(value) {
|
|
3515
|
-
try {
|
|
3516
|
-
return decodeBase64Json(value);
|
|
3517
|
-
} catch {
|
|
3518
|
-
try {
|
|
3519
|
-
return JSON.parse(value);
|
|
3520
|
-
} catch {
|
|
3521
|
-
return void 0;
|
|
3522
|
-
}
|
|
3523
|
-
}
|
|
3524
|
-
}
|
|
3525
|
-
async function captureMerchantResponse(response) {
|
|
3526
|
-
const merchant_body = await response.text().catch(() => "");
|
|
3527
|
-
return {
|
|
3528
|
-
merchant_status: response.status,
|
|
3529
|
-
merchant_status_text: response.statusText,
|
|
3530
|
-
merchant_headers: Object.fromEntries(response.headers.entries()),
|
|
3531
|
-
merchant_body
|
|
3532
|
-
};
|
|
3533
|
-
}
|
|
3534
3699
|
|
|
3535
3700
|
// src/tool-descriptions.ts
|
|
3536
3701
|
function composeDescription(d) {
|
|
@@ -3574,9 +3739,9 @@ var toolDescriptions = {
|
|
|
3574
3739
|
nextActionGuidance: ""
|
|
3575
3740
|
},
|
|
3576
3741
|
getAgent: {
|
|
3577
|
-
summary: "Return the authenticated agent identity AND its live spend authority in one call: Haven wallet, delegate, chain, raw status,
|
|
3742
|
+
summary: "Return the authenticated agent identity AND its live spend authority in one call: Haven wallet, delegate, chain, raw status, spend_authority_readiness, and per-token remaining allowance (atomic + human-readable). The recommended first call in a new session to confirm who you are and whether Haven will let you spend right now.",
|
|
3578
3743
|
selectionGuidance: "Use this as the one-shot orientation/bootstrap at the start of a session, or whenever you need to confirm identity together with whether the agent can spend right now. For a detailed per-token breakdown (configured vs spent vs reset window) use haven_get_allowances.",
|
|
3579
|
-
behavior: 'Reads identity plus the live spend-authority snapshot in one shot \u2014 the on-chain AllowanceModule on the legacy rail, the active budget delegation on the delegation rail. readiness is "ready" when at least one token has remaining spend authority, "needs_approval" when the agent is active but has none, and "revoked" when the credential is not active. What an over-budget payment does differs by rail: on the legacy AllowanceModule rail it is queued for the wallet owner to approve in Haven; on the delegation rail there is no approval queue \u2014 an over-budget redemption reverts on-chain, so ask the owner to grant or raise the budget in Haven rather than waiting for an approval. allowances[] carries remainingAtomic and remainingDisplay per token. Identity fields (id, name, status, safeAddress, delegateAddress, chainId) are unchanged from before.',
|
|
3744
|
+
behavior: 'Reads identity plus the live spend-authority snapshot in one shot \u2014 the on-chain AllowanceModule on the legacy rail, the active budget delegation on the delegation rail. spend_authority_readiness (readiness is a deprecated alias, same value) is "ready" when at least one token has remaining spend authority, "needs_approval" when the agent is active but has none, and "revoked" when the credential is not active. It covers hosted identity + on-chain spend authority ONLY \u2014 the hosted server cannot see the LOCAL signer, so "ready" does not mean the signer can start; verify the signer with a signer tool call or connect --doctor. What an over-budget payment does differs by rail: on the legacy AllowanceModule rail it is queued for the wallet owner to approve in Haven; on the delegation rail there is no approval queue \u2014 an over-budget redemption reverts on-chain, so ask the owner to grant or raise the budget in Haven rather than waiting for an approval. allowances[] carries remainingAtomic and remainingDisplay per token. Identity fields (id, name, status, safeAddress, delegateAddress, chainId) are unchanged from before.',
|
|
3580
3745
|
nextActionGuidance: ""
|
|
3581
3746
|
},
|
|
3582
3747
|
getAllowances: {
|
|
@@ -3607,7 +3772,7 @@ var toolDescriptions = {
|
|
|
3607
3772
|
summary: "Step 1 of a purchase: discover payable services from Haven's curated merchant catalog \u2014 names, prices, and which pay tool to use next.",
|
|
3608
3773
|
selectionGuidance: "Use this when the user asks what the agent can buy, pay for, or which paid services exist \u2014 or when you need a resource URL for a service the user described. Do NOT use for balance, budget, or spend-limit questions \u2014 use haven_get_allowances. Do NOT use to pay \u2014 each returned entry names the pay tool to use next.",
|
|
3609
3774
|
behavior: "Use each entry's suggested_tool field first \u2014 it names the exact next call. Read-only lookup against Haven's curated catalog; entries are periodically re-verified against the live merchant and degraded entries are flagged. Use category for a case-insensitive category filter (for example, VPN or vpn), or search for a product name, category, or description term. Returns name, description, price, rail, resource URL, tool_name, tool_arguments, and suggested_tool. The catalog price (price_display/price_atomic, marked price_is_indicative) is a last-verified hint, NOT authoritative \u2014 the real price comes from the merchant's live 402 at pay time. Never creates a payment, signature, or approval.",
|
|
3610
|
-
nextActionGuidance: `Pick an entry and pay it with the tool named in suggested_tool, passing the entry's resource_url, tool_name, and tool_arguments for MCP merchants. Confirm the price from the live pay-tool result (not the catalog), and pass the user's cap as max_amount_human in whole tokens ("no more than 1 USDC" \u2192 max_amount_human: "1") \u2014 never convert it to atomic units by hand
|
|
3775
|
+
nextActionGuidance: `Pick an entry and pay it with the tool named in suggested_tool, passing the entry's resource_url, tool_name, and tool_arguments for MCP merchants. Confirm the price from the live pay-tool result (not the catalog), and pass the user's cap as max_amount_human in whole tokens ("no more than 1 USDC" \u2192 max_amount_human: "1") \u2014 never convert it to atomic units by hand.`
|
|
3611
3776
|
},
|
|
3612
3777
|
sweep_delegate: {
|
|
3613
3778
|
summary: "Sweep stranded USDC and/or ETH from the delegate wallet back to the originating Safe.",
|
|
@@ -3852,9 +4017,13 @@ user's approval in Haven.
|
|
|
3852
4017
|
|
|
3853
4018
|
Hosted tools run in the \`mcp__haven__\` namespace. Local signing tools run in
|
|
3854
4019
|
the \`mcp__haven-signer__\` namespace and keep the delegate key on this machine.
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
4020
|
+
That namespacing is Claude-family; other runtimes name the servers by their
|
|
4021
|
+
own config keys (Codex: \`haven\`, \`haven_signer\`). Tool results carry the
|
|
4022
|
+
exact next step (\`next_action\`, \`next_tool\`, \`next_arguments\`, plus the
|
|
4023
|
+
runtime-neutral \`next_tool_server\` + \`next_tool_name\` \u2014 the bare tool name
|
|
4024
|
+
on that logical server, whatever your runtime calls it).
|
|
4025
|
+
Follow those fields first; the prose below is fallback and orientation, not
|
|
4026
|
+
the source of truth.
|
|
3858
4027
|
|
|
3859
4028
|
## When to use this skill
|
|
3860
4029
|
|
|
@@ -3880,8 +4049,10 @@ Before any payment, confirm the *live remaining* budget with the tools \u2014
|
|
|
3880
4049
|
spending:
|
|
3881
4050
|
|
|
3882
4051
|
- \`mcp__haven__haven_get_agent\` \u2014 the recommended first call: identity
|
|
3883
|
-
(wallet, network) plus
|
|
3884
|
-
\`revoked\`) and live remaining per-token allowance, in one shot.
|
|
4052
|
+
(wallet, network) plus \`spend_authority_readiness\` (\`ready\` / \`needs_approval\` /
|
|
4053
|
+
\`revoked\`) and live remaining per-token allowance, in one shot. That signal
|
|
4054
|
+
covers hosted identity and on-chain spend authority only \u2014 it cannot see the
|
|
4055
|
+
local signer; the signer is verified by calling any signer tool.
|
|
3885
4056
|
- \`mcp__haven__haven_get_allowances\` \u2014 detailed per-token breakdown
|
|
3886
4057
|
(configured, spent, reset window) when you need more than the summary.
|
|
3887
4058
|
|