@heraldprotocol/mpp 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -0
- package/dist/cjs/client.cjs +242 -0
- package/dist/cjs/client.cjs.map +1 -0
- package/dist/cjs/client.d.cts +112 -0
- package/dist/cjs/index.cjs +71 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +54 -0
- package/dist/cjs/server.cjs +397 -0
- package/dist/cjs/server.cjs.map +1 -0
- package/dist/cjs/server.d.cts +84 -0
- package/dist/cjs/types-CKXzdGiJ.d.cts +3 -0
- package/dist/esm/chunk-CRRGQQSN.js +45 -0
- package/dist/esm/chunk-CRRGQQSN.js.map +1 -0
- package/dist/esm/chunk-XBFBAAXJ.js +87 -0
- package/dist/esm/chunk-XBFBAAXJ.js.map +1 -0
- package/dist/esm/client.d.ts +112 -0
- package/dist/esm/client.js +150 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/index.d.ts +54 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/server.d.ts +84 -0
- package/dist/esm/server.js +260 -0
- package/dist/esm/server.js.map +1 -0
- package/dist/esm/types-CKXzdGiJ.d.ts +3 -0
- package/package.json +61 -0
- package/src/Methods.ts +43 -0
- package/src/client/Charge.ts +195 -0
- package/src/client/Methods.ts +29 -0
- package/src/client/index.ts +2 -0
- package/src/defaults.ts +100 -0
- package/src/index.ts +1 -0
- package/src/server/Charge.ts +382 -0
- package/src/server/Methods.ts +29 -0
- package/src/server/index.ts +2 -0
- package/src/types.ts +1 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chainId,
|
|
3
|
+
decimals,
|
|
4
|
+
erc3009Abi,
|
|
5
|
+
erc3009Tokens,
|
|
6
|
+
resolveCurrency,
|
|
7
|
+
rpcUrl
|
|
8
|
+
} from "./chunk-XBFBAAXJ.js";
|
|
9
|
+
import {
|
|
10
|
+
charge
|
|
11
|
+
} from "./chunk-CRRGQQSN.js";
|
|
12
|
+
|
|
13
|
+
// src/server/Charge.ts
|
|
14
|
+
import { Method, Store } from "mppx";
|
|
15
|
+
import {
|
|
16
|
+
encodeFunctionData,
|
|
17
|
+
erc20Abi,
|
|
18
|
+
isAddressEqual,
|
|
19
|
+
keccak256,
|
|
20
|
+
parseEventLogs
|
|
21
|
+
} from "viem";
|
|
22
|
+
import { parseAccount } from "viem/accounts";
|
|
23
|
+
import {
|
|
24
|
+
getTransactionReceipt,
|
|
25
|
+
sendTransaction,
|
|
26
|
+
sendTransactionSync
|
|
27
|
+
} from "viem/actions";
|
|
28
|
+
function charge2(parameters = {}) {
|
|
29
|
+
const {
|
|
30
|
+
amount,
|
|
31
|
+
currency = resolveCurrency(parameters),
|
|
32
|
+
decimals: decimals2 = decimals,
|
|
33
|
+
description,
|
|
34
|
+
externalId,
|
|
35
|
+
recipient,
|
|
36
|
+
waitForConfirmation = true
|
|
37
|
+
} = parameters;
|
|
38
|
+
const store = parameters.store ?? Store.memory();
|
|
39
|
+
if (currency.toLowerCase() in erc3009Tokens && !parameters.account) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"ERC-3009 requires an `account` parameter so the server can sign and broadcast the transferWithAuthorization transaction."
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const serverAccount = parameters.account ? typeof parameters.account === "string" ? parseAccount(parameters.account) : parameters.account : void 0;
|
|
45
|
+
const resolveClient = async (chainId2) => {
|
|
46
|
+
if (parameters.getClient) return parameters.getClient({ chainId: chainId2 });
|
|
47
|
+
const id = chainId2 ?? chainId.mainnet;
|
|
48
|
+
const url = rpcUrl[id];
|
|
49
|
+
if (!url) throw new Error(`No RPC URL configured for chainId ${id}.`);
|
|
50
|
+
const { createClient, http } = await import("viem");
|
|
51
|
+
return createClient({ chain: { id }, transport: http(url) });
|
|
52
|
+
};
|
|
53
|
+
return Method.toServer(charge, {
|
|
54
|
+
defaults: {
|
|
55
|
+
amount,
|
|
56
|
+
currency,
|
|
57
|
+
decimals: decimals2,
|
|
58
|
+
description,
|
|
59
|
+
externalId,
|
|
60
|
+
recipient
|
|
61
|
+
},
|
|
62
|
+
async request({ request }) {
|
|
63
|
+
const chainId2 = await (async () => {
|
|
64
|
+
if (request.chainId) return request.chainId;
|
|
65
|
+
if (parameters.testnet) return chainId.testnet;
|
|
66
|
+
return (await resolveClient(void 0)).chain?.id;
|
|
67
|
+
})();
|
|
68
|
+
const client = await (async () => {
|
|
69
|
+
try {
|
|
70
|
+
return await resolveClient(chainId2);
|
|
71
|
+
} catch {
|
|
72
|
+
throw new Error(`No client configured with chainId ${chainId2}.`);
|
|
73
|
+
}
|
|
74
|
+
})();
|
|
75
|
+
if (client.chain?.id !== chainId2)
|
|
76
|
+
throw new Error(`Client not configured with chainId ${chainId2}.`);
|
|
77
|
+
return { ...request, chainId: chainId2 };
|
|
78
|
+
},
|
|
79
|
+
async verify({ credential, request }) {
|
|
80
|
+
const { challenge } = credential;
|
|
81
|
+
const { chainId: chainId2 } = request;
|
|
82
|
+
const client = await resolveClient(chainId2);
|
|
83
|
+
const { request: challengeRequest } = challenge;
|
|
84
|
+
const challengeAmount = challengeRequest.amount;
|
|
85
|
+
const challengeCurrency = challengeRequest.currency;
|
|
86
|
+
const challengeRecipient = challengeRequest.recipient;
|
|
87
|
+
const expires = challenge.expires;
|
|
88
|
+
if (expires && new Date(expires) < /* @__PURE__ */ new Date()) {
|
|
89
|
+
throw new Error(`Payment expired at ${expires}.`);
|
|
90
|
+
}
|
|
91
|
+
const payload = credential.payload;
|
|
92
|
+
switch (payload.type) {
|
|
93
|
+
case "hash": {
|
|
94
|
+
const hash = payload.hash;
|
|
95
|
+
await assertHashUnused(store, hash);
|
|
96
|
+
const sender = extractDidAddress(credential.source);
|
|
97
|
+
if (!sender)
|
|
98
|
+
throw new Error(
|
|
99
|
+
"Hash credential is missing a valid `source` DID \u2014 cannot verify sender."
|
|
100
|
+
);
|
|
101
|
+
const receipt = await getTransactionReceipt(client, { hash });
|
|
102
|
+
const transferLogs = parseEventLogs({
|
|
103
|
+
abi: erc20Abi,
|
|
104
|
+
eventName: "Transfer",
|
|
105
|
+
logs: receipt.logs
|
|
106
|
+
});
|
|
107
|
+
const match = transferLogs.find(
|
|
108
|
+
(log) => isAddressEqual(log.address, challengeCurrency) && isAddressEqual(log.args.from, sender) && isAddressEqual(log.args.to, challengeRecipient) && log.args.value.toString() === challengeAmount
|
|
109
|
+
);
|
|
110
|
+
if (!match)
|
|
111
|
+
throw new MismatchError(
|
|
112
|
+
"Payment verification failed: no matching ERC-20 transfer found.",
|
|
113
|
+
{
|
|
114
|
+
sender,
|
|
115
|
+
amount: challengeAmount,
|
|
116
|
+
currency: challengeCurrency,
|
|
117
|
+
recipient: challengeRecipient
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
await markHashUsed(store, hash);
|
|
121
|
+
return toReceipt(receipt);
|
|
122
|
+
}
|
|
123
|
+
case "authorization": {
|
|
124
|
+
if (!serverAccount) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
"Received ERC-3009 authorization credential but no server `account` is configured. Set `account` in charge parameters to broadcast transferWithAuthorization."
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const { from, to, value, validAfter, validBefore, nonce, signature } = payload;
|
|
130
|
+
const r = `0x${signature.slice(2, 66)}`;
|
|
131
|
+
const s = `0x${signature.slice(66, 130)}`;
|
|
132
|
+
const v = parseInt(signature.slice(130, 132), 16);
|
|
133
|
+
if (!isAddressEqual(to, challengeRecipient))
|
|
134
|
+
throw new MismatchError(
|
|
135
|
+
"Authorization recipient does not match challenge.",
|
|
136
|
+
{ expected: challengeRecipient, actual: to }
|
|
137
|
+
);
|
|
138
|
+
if (value !== challengeAmount)
|
|
139
|
+
throw new MismatchError(
|
|
140
|
+
"Authorization amount does not match challenge.",
|
|
141
|
+
{ expected: challengeAmount, actual: value }
|
|
142
|
+
);
|
|
143
|
+
const validBeforeTs = Number(validBefore);
|
|
144
|
+
if (validBeforeTs > 0 && validBeforeTs < Math.floor(Date.now() / 1e3)) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`ERC-3009 authorization expired (validBefore: ${validBefore}).`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const hash = keccak256(signature);
|
|
150
|
+
await assertHashUnused(store, hash);
|
|
151
|
+
await markHashUsed(store, hash);
|
|
152
|
+
if (waitForConfirmation) {
|
|
153
|
+
const receipt = await sendTransactionSync(client, {
|
|
154
|
+
account: serverAccount,
|
|
155
|
+
chain: client.chain,
|
|
156
|
+
to: challengeCurrency,
|
|
157
|
+
data: encodeFunctionData({
|
|
158
|
+
abi: erc3009Abi,
|
|
159
|
+
functionName: "transferWithAuthorization",
|
|
160
|
+
args: [
|
|
161
|
+
from,
|
|
162
|
+
to,
|
|
163
|
+
BigInt(value),
|
|
164
|
+
BigInt(validAfter),
|
|
165
|
+
BigInt(validBefore),
|
|
166
|
+
nonce,
|
|
167
|
+
v,
|
|
168
|
+
r,
|
|
169
|
+
s
|
|
170
|
+
]
|
|
171
|
+
})
|
|
172
|
+
});
|
|
173
|
+
return toReceipt(receipt);
|
|
174
|
+
}
|
|
175
|
+
const txHash = await sendTransaction(client, {
|
|
176
|
+
account: serverAccount,
|
|
177
|
+
chain: client.chain,
|
|
178
|
+
to: challengeCurrency,
|
|
179
|
+
data: encodeFunctionData({
|
|
180
|
+
abi: erc3009Abi,
|
|
181
|
+
functionName: "transferWithAuthorization",
|
|
182
|
+
args: [
|
|
183
|
+
from,
|
|
184
|
+
to,
|
|
185
|
+
BigInt(value),
|
|
186
|
+
BigInt(validAfter),
|
|
187
|
+
BigInt(validBefore),
|
|
188
|
+
nonce,
|
|
189
|
+
v,
|
|
190
|
+
r,
|
|
191
|
+
s
|
|
192
|
+
]
|
|
193
|
+
})
|
|
194
|
+
});
|
|
195
|
+
return {
|
|
196
|
+
method: "zerog",
|
|
197
|
+
status: "success",
|
|
198
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
199
|
+
reference: txHash
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
default:
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Unsupported credential type "${payload.type}".`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function getHashStoreKey(hash) {
|
|
211
|
+
return `mppx:charge:${hash.toLowerCase()}`;
|
|
212
|
+
}
|
|
213
|
+
async function assertHashUnused(store, hash) {
|
|
214
|
+
const seen = await store.get(getHashStoreKey(hash));
|
|
215
|
+
if (seen !== null) throw new Error("Transaction hash has already been used.");
|
|
216
|
+
}
|
|
217
|
+
async function markHashUsed(store, hash) {
|
|
218
|
+
await store.put(getHashStoreKey(hash), Date.now());
|
|
219
|
+
}
|
|
220
|
+
function toReceipt(receipt) {
|
|
221
|
+
const { status, transactionHash } = receipt;
|
|
222
|
+
if (status !== "success") {
|
|
223
|
+
throw new Error(`Transaction reverted: ${transactionHash}`);
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
method: "zerog",
|
|
227
|
+
status: "success",
|
|
228
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
229
|
+
reference: transactionHash
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function extractDidAddress(source) {
|
|
233
|
+
if (!source) return void 0;
|
|
234
|
+
const match = /^did:pkh:eip155:\d+:(0x[0-9a-fA-F]{40})$/.exec(source);
|
|
235
|
+
return match ? match[1] : void 0;
|
|
236
|
+
}
|
|
237
|
+
var MismatchError = class extends Error {
|
|
238
|
+
constructor(reason, details) {
|
|
239
|
+
super(
|
|
240
|
+
[
|
|
241
|
+
reason,
|
|
242
|
+
...Object.entries(details).map(([k, v]) => ` - ${k}: ${v}`)
|
|
243
|
+
].join("\n")
|
|
244
|
+
);
|
|
245
|
+
this.name = "MismatchError";
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// src/server/Methods.ts
|
|
250
|
+
function zerog(parameters) {
|
|
251
|
+
return [zerog.charge(parameters)];
|
|
252
|
+
}
|
|
253
|
+
((zerog2) => {
|
|
254
|
+
zerog2.charge = charge2;
|
|
255
|
+
})(zerog || (zerog = {}));
|
|
256
|
+
export {
|
|
257
|
+
charge2 as charge,
|
|
258
|
+
zerog
|
|
259
|
+
};
|
|
260
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/server/Charge.ts","../../src/server/Methods.ts"],"sourcesContent":["import type { Account, Address, Chain, Client, TransactionReceipt } from \"viem\";\nimport { Method, Store } from \"mppx\";\nimport {\n encodeFunctionData,\n erc20Abi,\n isAddressEqual,\n keccak256,\n parseEventLogs,\n} from \"viem\";\nimport { parseAccount } from \"viem/accounts\";\nimport {\n getTransactionReceipt,\n sendTransaction,\n sendTransactionSync,\n} from \"viem/actions\";\n\nimport type { MaybePromise } from \"../types.js\";\nimport * as defaults from \"../defaults.js\";\nimport * as Methods from \"../Methods.js\";\n\n/**\n * Creates a 0G charge method intent for usage on the server.\n *\n * @example\n * ```ts\n * import { zerog } from \"@heraldprotocol/mpp/server\";\n *\n * const charge = zerog.charge({\n * recipient: \"0x...\",\n * currency: \"0x...\",\n * account: privateKeyToAccount(\"0x...\"),\n * });\n * ```\n */\nexport function charge(parameters: charge.Parameters = {}): Method.AnyServer {\n const {\n amount,\n currency = defaults.resolveCurrency(parameters),\n decimals = defaults.decimals,\n description,\n externalId,\n recipient,\n waitForConfirmation = true,\n } = parameters;\n const store = (parameters.store ??\n Store.memory()) as Store.Store<charge.StoreItemMap>;\n\n if (currency.toLowerCase() in defaults.erc3009Tokens && !parameters.account) {\n throw new Error(\n \"ERC-3009 requires an `account` parameter so the server can sign and broadcast \" +\n \"the transferWithAuthorization transaction.\"\n );\n }\n\n const serverAccount = parameters.account\n ? typeof parameters.account === \"string\"\n ? parseAccount(parameters.account)\n : parameters.account\n : undefined;\n\n const resolveClient = async (\n chainId?: number | undefined\n ): Promise<Client> => {\n if (parameters.getClient) return parameters.getClient({ chainId });\n const id = chainId ?? defaults.chainId.mainnet;\n const url = defaults.rpcUrl[id];\n if (!url) throw new Error(`No RPC URL configured for chainId ${id}.`);\n const { createClient, http } = await import(\"viem\");\n return createClient({ chain: { id } as Chain, transport: http(url) });\n };\n\n return Method.toServer(Methods.charge, {\n defaults: {\n amount,\n currency,\n decimals,\n description,\n externalId,\n recipient,\n } as never,\n\n async request({ request }) {\n const chainId = await (async () => {\n if (request.chainId) return request.chainId;\n if (parameters.testnet) return defaults.chainId.testnet;\n return (await resolveClient(undefined)).chain?.id;\n })();\n\n const client = await (async () => {\n try {\n return await resolveClient(chainId);\n } catch {\n throw new Error(`No client configured with chainId ${chainId}.`);\n }\n })();\n if (client.chain?.id !== chainId)\n throw new Error(`Client not configured with chainId ${chainId}.`);\n\n return { ...request, chainId };\n },\n\n async verify({ credential, request }) {\n const { challenge } = credential;\n const { chainId } = request;\n\n const client = await resolveClient(chainId);\n\n const { request: challengeRequest } = challenge;\n const challengeAmount = challengeRequest.amount as string;\n const challengeCurrency = challengeRequest.currency as Address;\n const challengeRecipient = challengeRequest.recipient as Address;\n const expires = challenge.expires;\n\n if (expires && new Date(expires) < new Date()) {\n throw new Error(`Payment expired at ${expires}.`);\n }\n\n const payload = credential.payload;\n\n switch (payload.type) {\n case \"hash\": {\n const hash = payload.hash as `0x${string}`;\n await assertHashUnused(store, hash);\n\n const sender = extractDidAddress(credential.source);\n if (!sender)\n throw new Error(\n \"Hash credential is missing a valid `source` DID — cannot verify sender.\"\n );\n\n const receipt = await getTransactionReceipt(client, { hash });\n\n const transferLogs = parseEventLogs({\n abi: erc20Abi,\n eventName: \"Transfer\",\n logs: receipt.logs,\n });\n\n const match = transferLogs.find(\n (log) =>\n isAddressEqual(log.address, challengeCurrency) &&\n isAddressEqual(log.args.from, sender) &&\n isAddressEqual(log.args.to, challengeRecipient) &&\n log.args.value.toString() === challengeAmount\n );\n\n if (!match)\n throw new MismatchError(\n \"Payment verification failed: no matching ERC-20 transfer found.\",\n {\n sender,\n amount: challengeAmount,\n currency: challengeCurrency,\n recipient: challengeRecipient,\n }\n );\n\n await markHashUsed(store, hash);\n\n return toReceipt(receipt);\n }\n\n case \"authorization\": {\n if (!serverAccount) {\n throw new Error(\n \"Received ERC-3009 authorization credential but no server `account` is configured. \" +\n \"Set `account` in charge parameters to broadcast transferWithAuthorization.\"\n );\n }\n\n const { from, to, value, validAfter, validBefore, nonce, signature } =\n payload as {\n from: string;\n to: string;\n value: string;\n validAfter: string;\n validBefore: string;\n nonce: string;\n signature: string;\n };\n\n // Split signature into v, r, s for the contract call\n const r = `0x${signature.slice(2, 66)}` as `0x${string}`;\n const s = `0x${signature.slice(66, 130)}` as `0x${string}`;\n const v = parseInt(signature.slice(130, 132), 16);\n\n // Validate authorization parameters match the challenge\n if (!isAddressEqual(to as Address, challengeRecipient))\n throw new MismatchError(\n \"Authorization recipient does not match challenge.\",\n { expected: challengeRecipient, actual: to }\n );\n\n if (value !== challengeAmount)\n throw new MismatchError(\n \"Authorization amount does not match challenge.\",\n { expected: challengeAmount, actual: value }\n );\n\n // Check expiry from the authorization itself\n const validBeforeTs = Number(validBefore);\n if (\n validBeforeTs > 0 &&\n validBeforeTs < Math.floor(Date.now() / 1000)\n ) {\n throw new Error(\n `ERC-3009 authorization expired (validBefore: ${validBefore}).`\n );\n }\n\n const hash = keccak256(signature as `0x${string}`);\n await assertHashUnused(store, hash);\n await markHashUsed(store, hash);\n\n if (waitForConfirmation) {\n const receipt = await sendTransactionSync(client, {\n account: serverAccount,\n chain: client.chain,\n to: challengeCurrency,\n data: encodeFunctionData({\n abi: defaults.erc3009Abi,\n functionName: \"transferWithAuthorization\",\n args: [\n from as Address,\n to as Address,\n BigInt(value),\n BigInt(validAfter),\n BigInt(validBefore),\n nonce as `0x${string}`,\n v,\n r as `0x${string}`,\n s as `0x${string}`,\n ],\n }),\n } as never);\n\n return toReceipt(receipt);\n }\n\n const txHash = await sendTransaction(client, {\n account: serverAccount,\n chain: client.chain,\n to: challengeCurrency,\n data: encodeFunctionData({\n abi: defaults.erc3009Abi,\n functionName: \"transferWithAuthorization\",\n args: [\n from as Address,\n to as Address,\n BigInt(value),\n BigInt(validAfter),\n BigInt(validBefore),\n nonce as `0x${string}`,\n v,\n r as `0x${string}`,\n s as `0x${string}`,\n ],\n }),\n } as never);\n\n return {\n method: \"zerog\" as const,\n status: \"success\" as const,\n timestamp: new Date().toISOString(),\n reference: txHash,\n };\n }\n\n default:\n throw new Error(\n `Unsupported credential type \"${(payload as { type: string }).type}\".`\n );\n }\n },\n });\n}\n\nexport declare namespace charge {\n type StoreItemMap = {\n [key: `mppx:charge:${string}`]: number;\n };\n\n type Parameters = {\n /** Default payment amount (human-readable, e.g. \"1.50\"). */\n amount?: string | undefined;\n /** ERC-20 token contract address. */\n currency?: string | undefined;\n /** Token decimals. @default 6 */\n decimals?: number | undefined;\n /** Human-readable description. */\n description?: string | undefined;\n /** External identifier to echo back in receipt. */\n externalId?: string | undefined;\n /** Recipient address for payments. */\n recipient?: string | undefined;\n /** Testnet mode. */\n testnet?: boolean | undefined;\n /**\n * Whether to wait for the charge transaction to confirm on-chain.\n * @default true\n */\n waitForConfirmation?: boolean | undefined;\n /** Function that returns a viem Client for the given chain ID. */\n getClient?:\n | ((parameters: { chainId?: number | undefined }) => MaybePromise<Client>)\n | undefined;\n /**\n * Server account used to broadcast `transferWithAuthorization` transactions.\n * Required when accepting `authorization` payloads. The server pays gas\n * from this account.\n */\n account?: Account | Address | undefined;\n /**\n * Store for transaction hash replay protection.\n *\n * Use a shared store in multi-instance deployments so consumed hashes are\n * visible across all server instances.\n */\n store?: Store.Store | undefined;\n };\n}\n\n/** @internal */\nfunction getHashStoreKey(hash: `0x${string}`): `mppx:charge:${string}` {\n return `mppx:charge:${hash.toLowerCase()}`;\n}\n\n/** @internal */\nasync function assertHashUnused(\n store: Store.Store<charge.StoreItemMap>,\n hash: `0x${string}`\n): Promise<void> {\n const seen = await store.get(getHashStoreKey(hash));\n if (seen !== null) throw new Error(\"Transaction hash has already been used.\");\n}\n\n/** @internal */\nasync function markHashUsed(\n store: Store.Store<charge.StoreItemMap>,\n hash: `0x${string}`\n): Promise<void> {\n await store.put(getHashStoreKey(hash), Date.now());\n}\n\n/** @internal */\nfunction toReceipt(receipt: TransactionReceipt) {\n const { status, transactionHash } = receipt;\n if (status !== \"success\") {\n throw new Error(`Transaction reverted: ${transactionHash}`);\n }\n return {\n method: \"zerog\" as const,\n status: \"success\" as const,\n timestamp: new Date().toISOString(),\n reference: transactionHash,\n };\n}\n\n/**\n * Extracts an Ethereum address from a `did:pkh:eip155:<chainId>:<address>` DID.\n * Returns `undefined` if the source is missing or malformed.\n * @internal\n */\nfunction extractDidAddress(source: string | undefined): Address | undefined {\n if (!source) return undefined;\n const match = /^did:pkh:eip155:\\d+:(0x[0-9a-fA-F]{40})$/.exec(source);\n return match ? (match[1] as Address) : undefined;\n}\n\n/** @internal */\nclass MismatchError extends Error {\n override readonly name = \"MismatchError\";\n\n constructor(reason: string, details: Record<string, string>) {\n super(\n [\n reason,\n ...Object.entries(details).map(([k, v]) => ` - ${k}: ${v}`),\n ].join(\"\\n\")\n );\n }\n}\n","import type { Method } from \"mppx\";\n\nimport { charge as charge_ } from \"./Charge.js\";\n\n/**\n * Creates a 0G `charge` server method.\n *\n * @example\n * ```ts\n * import { Mppx } from \"mppx/server\";\n * import { zerog } from \"@heraldprotocol/mpp/server\";\n *\n * const mppx = Mppx.create({\n * methods: [zerog({ recipient: \"0x...\", currency: \"0x...\" })],\n * });\n * ```\n */\nexport function zerog(\n parameters?: zerog.Parameters\n): readonly [Method.AnyServer] {\n return [zerog.charge(parameters)] as const;\n}\n\nexport namespace zerog {\n export type Parameters = charge_.Parameters;\n\n /** Creates a 0G `charge` method for one-time ERC-20 token transfers. */\n export const charge = charge_;\n}\n"],"mappings":"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","names":["charge","decimals","chainId","zerog","charge"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
+
"name": "@heraldprotocol/mpp",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "0G Chain payment method for the Machine Payments Protocol",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/heraldprotocol/protocol.git",
|
|
9
|
+
"directory": "packages/sdk/js/mpp"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/esm/index.d.ts",
|
|
18
|
+
"default": "./dist/esm/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./client": {
|
|
21
|
+
"types": "./dist/esm/client/index.d.ts",
|
|
22
|
+
"default": "./dist/esm/client/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./server": {
|
|
25
|
+
"types": "./dist/esm/server/index.d.ts",
|
|
26
|
+
"default": "./dist/esm/server/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"main": "./dist/esm/index.js",
|
|
30
|
+
"types": "./dist/esm/index.d.ts",
|
|
31
|
+
"sideEffects": false,
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"src/**/*.ts",
|
|
35
|
+
"!**/*.test.ts"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup",
|
|
39
|
+
"typecheck": "tsc --noEmit"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"zod": "catalog:"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"typescript": "catalog:",
|
|
46
|
+
"viem": ">=2.48.0",
|
|
47
|
+
"mppx": ">=0.4.8"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"typescript": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@biomejs/biome": "catalog:",
|
|
56
|
+
"@tsconfig/node22": "catalog:",
|
|
57
|
+
"@types/node": "catalog:",
|
|
58
|
+
"tsup": "8.5.1",
|
|
59
|
+
"typescript": "catalog:"
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/Methods.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Method, z } from "mppx";
|
|
2
|
+
import { parseUnits } from "viem";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 0G charge intent for one-time ERC-20 token transfers.
|
|
6
|
+
*/
|
|
7
|
+
export const charge = Method.from({
|
|
8
|
+
name: "zerog",
|
|
9
|
+
intent: "charge",
|
|
10
|
+
schema: {
|
|
11
|
+
credential: {
|
|
12
|
+
payload: z.discriminatedUnion("type", [
|
|
13
|
+
z.object({ hash: z.hash(), type: z.literal("hash") }),
|
|
14
|
+
z.object({
|
|
15
|
+
type: z.literal("authorization"),
|
|
16
|
+
from: z.string(),
|
|
17
|
+
to: z.string(),
|
|
18
|
+
value: z.string(),
|
|
19
|
+
validAfter: z.string(),
|
|
20
|
+
validBefore: z.string(),
|
|
21
|
+
nonce: z.string(),
|
|
22
|
+
signature: z.string(),
|
|
23
|
+
}),
|
|
24
|
+
]),
|
|
25
|
+
},
|
|
26
|
+
request: z.pipe(
|
|
27
|
+
z.object({
|
|
28
|
+
amount: z.amount(),
|
|
29
|
+
chainId: z.optional(z.number()),
|
|
30
|
+
currency: z.string(),
|
|
31
|
+
decimals: z.number(),
|
|
32
|
+
description: z.optional(z.string()),
|
|
33
|
+
externalId: z.optional(z.string()),
|
|
34
|
+
recipient: z.optional(z.string()),
|
|
35
|
+
}),
|
|
36
|
+
z.transform(({ amount, chainId, decimals, ...rest }) => ({
|
|
37
|
+
...rest,
|
|
38
|
+
amount: parseUnits(amount, decimals).toString(),
|
|
39
|
+
...(chainId !== undefined ? { methodDetails: { chainId } } : {}),
|
|
40
|
+
}))
|
|
41
|
+
),
|
|
42
|
+
},
|
|
43
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { Account, Address, Chain, Client } from "viem";
|
|
2
|
+
import { Credential, Method, z } from "mppx";
|
|
3
|
+
import { encodeFunctionData, erc20Abi } from "viem";
|
|
4
|
+
import { parseAccount } from "viem/accounts";
|
|
5
|
+
import { sendTransactionSync, signTypedData } from "viem/actions";
|
|
6
|
+
|
|
7
|
+
import type { MaybePromise } from "../types.js";
|
|
8
|
+
import * as defaults from "../defaults.js";
|
|
9
|
+
import * as Methods from "../Methods.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a 0G charge method intent for usage on the client.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { zerog } from "@heraldprotocol/mpp/client";
|
|
17
|
+
* import { privateKeyToAccount } from "viem/accounts";
|
|
18
|
+
*
|
|
19
|
+
* const charge = zerog.charge({
|
|
20
|
+
* account: privateKeyToAccount("0x..."),
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function charge(parameters: charge.Parameters = {}) {
|
|
25
|
+
const resolveAccount = (
|
|
26
|
+
client: Client,
|
|
27
|
+
override?: Account | Address | undefined
|
|
28
|
+
): Account => {
|
|
29
|
+
const raw = override ?? parameters.account;
|
|
30
|
+
if (raw) return typeof raw === "string" ? parseAccount(raw) : raw;
|
|
31
|
+
if (client.account) return client.account;
|
|
32
|
+
throw new Error(
|
|
33
|
+
"No `account` provided. Pass `account` to parameters or context."
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const resolveClient = async (
|
|
38
|
+
chainId?: number | undefined
|
|
39
|
+
): Promise<Client> => {
|
|
40
|
+
if (parameters.getClient) return parameters.getClient({ chainId });
|
|
41
|
+
const id = chainId ?? defaults.chainId.mainnet;
|
|
42
|
+
const url = defaults.rpcUrl[id];
|
|
43
|
+
if (!url) throw new Error(`No RPC URL configured for chainId ${id}.`);
|
|
44
|
+
const { createClient, http } = await import("viem");
|
|
45
|
+
return createClient({ chain: { id } as Chain, transport: http(url) });
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return Method.toClient(Methods.charge, {
|
|
49
|
+
context: z.object({
|
|
50
|
+
account: z.optional(z.custom<Account | Address>()),
|
|
51
|
+
mode: z.optional(z.enum(["push", "pull"])),
|
|
52
|
+
}),
|
|
53
|
+
|
|
54
|
+
async createCredential({ challenge, context }) {
|
|
55
|
+
const chainId = challenge.request.methodDetails?.chainId as
|
|
56
|
+
| number
|
|
57
|
+
| undefined;
|
|
58
|
+
const client = await resolveClient(chainId);
|
|
59
|
+
const account = resolveAccount(client, context?.account);
|
|
60
|
+
|
|
61
|
+
const mode =
|
|
62
|
+
context?.mode ??
|
|
63
|
+
parameters.mode ??
|
|
64
|
+
(account.type === "json-rpc" ? "push" : "pull");
|
|
65
|
+
|
|
66
|
+
const { request } = challenge;
|
|
67
|
+
const amount = BigInt(request.amount);
|
|
68
|
+
const currency = request.currency as Address;
|
|
69
|
+
const recipient = request.recipient as Address;
|
|
70
|
+
|
|
71
|
+
if (mode === "pull") {
|
|
72
|
+
const tokenMeta = defaults.erc3009Tokens[currency.toLowerCase()];
|
|
73
|
+
if (!tokenMeta) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Token ${currency} does not support ERC-3009 (TransferWithAuthorization). ` +
|
|
76
|
+
`Cannot use pull mode.`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const resolvedChainId = chainId ?? client.chain?.id;
|
|
81
|
+
if (!resolvedChainId)
|
|
82
|
+
throw new Error("Could not determine chainId for EIP-712 domain.");
|
|
83
|
+
|
|
84
|
+
const { name: tokenName, version: tokenVersion } = tokenMeta;
|
|
85
|
+
|
|
86
|
+
const validAfter = 0n;
|
|
87
|
+
const validBefore = challenge.expires
|
|
88
|
+
? BigInt(Math.floor(new Date(challenge.expires).getTime() / 1000))
|
|
89
|
+
: BigInt(Math.floor(Date.now() / 1000) + 3600); // 1 hour default
|
|
90
|
+
const nonce = randomNonce();
|
|
91
|
+
|
|
92
|
+
const signature = await signTypedData(client, {
|
|
93
|
+
account,
|
|
94
|
+
domain: {
|
|
95
|
+
name: tokenName,
|
|
96
|
+
version: tokenVersion,
|
|
97
|
+
chainId: resolvedChainId,
|
|
98
|
+
verifyingContract: currency,
|
|
99
|
+
},
|
|
100
|
+
types: {
|
|
101
|
+
TransferWithAuthorization: [
|
|
102
|
+
{ name: "from", type: "address" },
|
|
103
|
+
{ name: "to", type: "address" },
|
|
104
|
+
{ name: "value", type: "uint256" },
|
|
105
|
+
{ name: "validAfter", type: "uint256" },
|
|
106
|
+
{ name: "validBefore", type: "uint256" },
|
|
107
|
+
{ name: "nonce", type: "bytes32" },
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
primaryType: "TransferWithAuthorization",
|
|
111
|
+
message: {
|
|
112
|
+
from: account.address,
|
|
113
|
+
to: recipient,
|
|
114
|
+
value: amount,
|
|
115
|
+
validAfter,
|
|
116
|
+
validBefore,
|
|
117
|
+
nonce,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return Credential.serialize({
|
|
122
|
+
challenge,
|
|
123
|
+
payload: {
|
|
124
|
+
type: "authorization" as const,
|
|
125
|
+
from: account.address,
|
|
126
|
+
to: recipient,
|
|
127
|
+
value: amount.toString(),
|
|
128
|
+
validAfter: validAfter.toString(),
|
|
129
|
+
validBefore: validBefore.toString(),
|
|
130
|
+
nonce,
|
|
131
|
+
signature,
|
|
132
|
+
},
|
|
133
|
+
source: `did:pkh:eip155:${resolvedChainId}:${account.address}`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (mode === "push") {
|
|
138
|
+
const receipt = await sendTransactionSync(client, {
|
|
139
|
+
account,
|
|
140
|
+
chain: client.chain,
|
|
141
|
+
to: currency,
|
|
142
|
+
data: encodeFunctionData({
|
|
143
|
+
abi: erc20Abi,
|
|
144
|
+
functionName: "transfer",
|
|
145
|
+
args: [recipient, amount],
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
const hash = receipt.transactionHash;
|
|
149
|
+
return Credential.serialize({
|
|
150
|
+
challenge,
|
|
151
|
+
payload: { hash, type: "hash" },
|
|
152
|
+
source: `did:pkh:eip155:${chainId ?? client.chain?.id}:${account.address}`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
throw new Error(`Unsupported mode: ${mode}`);
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export declare namespace charge {
|
|
162
|
+
type Parameters = {
|
|
163
|
+
/** Account to use for signing. */
|
|
164
|
+
account?: Account | Address | undefined;
|
|
165
|
+
/** Function that returns a viem Client for the given chain ID. */
|
|
166
|
+
getClient?:
|
|
167
|
+
| ((parameters: { chainId?: number | undefined }) => MaybePromise<Client>)
|
|
168
|
+
| undefined;
|
|
169
|
+
/**
|
|
170
|
+
* Controls how the charge transaction is submitted.
|
|
171
|
+
*
|
|
172
|
+
* - `'push'`: Client broadcasts the transaction and sends the tx hash.
|
|
173
|
+
* - `'pull'`: Client signs an ERC-3009 TransferWithAuthorization
|
|
174
|
+
* message. Server calls `transferWithAuthorization` and pays gas.
|
|
175
|
+
*
|
|
176
|
+
* @default `'push'` for JSON-RPC accounts, `'pull'` for local accounts.
|
|
177
|
+
*/
|
|
178
|
+
mode?: "push" | "pull" | undefined;
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Generates a random 32-byte hex nonce. */
|
|
183
|
+
function randomNonce(): `0x${string}` {
|
|
184
|
+
const bytes = new Uint8Array(32);
|
|
185
|
+
if (typeof crypto !== "undefined") {
|
|
186
|
+
crypto.getRandomValues(bytes);
|
|
187
|
+
} else if (typeof globalThis !== "undefined" && "crypto" in globalThis) {
|
|
188
|
+
(globalThis as unknown as { crypto: Crypto }).crypto.getRandomValues(bytes);
|
|
189
|
+
} else {
|
|
190
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
191
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return `0x${[...bytes].map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
195
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Method } from "mppx";
|
|
2
|
+
|
|
3
|
+
import { charge as charge_ } from "./Charge.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a 0G `charge` client method.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { Mppx } from "mppx/client";
|
|
11
|
+
* import { zerog } from "@heraldprotocol/mpp/client";
|
|
12
|
+
*
|
|
13
|
+
* const mppx = Mppx.create({
|
|
14
|
+
* methods: [zerog({ account })],
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function zerog(
|
|
19
|
+
parameters: zerog.Parameters = {}
|
|
20
|
+
): readonly [Method.AnyClient] {
|
|
21
|
+
return [charge_(parameters)] as const;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export namespace zerog {
|
|
25
|
+
export type Parameters = charge_.Parameters;
|
|
26
|
+
|
|
27
|
+
/** Creates a 0G `charge` client method for one-time ERC-20 token transfers. */
|
|
28
|
+
export const charge = charge_;
|
|
29
|
+
}
|