moltspay 0.2.2 → 0.2.4

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.
@@ -30,13 +30,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/wallet/index.ts
31
31
  var wallet_exports = {};
32
32
  __export(wallet_exports, {
33
+ AllowanceWallet: () => AllowanceWallet,
34
+ PermitSigner: () => PermitSigner,
33
35
  PermitWallet: () => PermitWallet,
34
36
  SecureWallet: () => SecureWallet,
35
37
  Wallet: () => Wallet,
36
38
  createWallet: () => createWallet,
37
39
  formatPermitRequest: () => formatPermitRequest,
40
+ generatePermitInstructions: () => generatePermitInstructions,
38
41
  getWalletAddress: () => getWalletAddress,
39
42
  loadWallet: () => loadWallet,
43
+ signPermit: () => signPermit,
40
44
  walletExists: () => walletExists
41
45
  });
42
46
  module.exports = __toCommonJS(wallet_exports);
@@ -976,15 +980,363 @@ After signing, send { v, r, s, deadline } to the Agent.
976
980
 
977
981
  \u26A0\uFE0F Note: This authorization only allows the Agent to spend up to ${amount} USDC from your wallet. Your private key is never exposed.`;
978
982
  }
983
+
984
+ // src/wallet/signPermit.ts
985
+ var import_ethers4 = require("ethers");
986
+ async function signPermit(config, params) {
987
+ const chain = config.chain || "base";
988
+ const chainConfig = getChain(chain);
989
+ const privateKey = config.privateKey || process.env.PAYMENT_AGENT_PRIVATE_KEY;
990
+ if (!privateKey) {
991
+ throw new Error("privateKey is required");
992
+ }
993
+ const rpcUrl = config.rpcUrl || chainConfig.rpc;
994
+ const provider = new import_ethers4.ethers.JsonRpcProvider(rpcUrl);
995
+ const wallet = new import_ethers4.ethers.Wallet(privateKey, provider);
996
+ const usdcContract = new import_ethers4.ethers.Contract(chainConfig.usdc, ERC20_ABI, provider);
997
+ const nonce = Number(await usdcContract.nonces(wallet.address));
998
+ let deadline;
999
+ if (!params.deadline) {
1000
+ deadline = Math.floor(Date.now() / 1e3) + 30 * 60;
1001
+ } else if (params.deadline < 1e6) {
1002
+ deadline = Math.floor(Date.now() / 1e3) + params.deadline * 60;
1003
+ } else {
1004
+ deadline = params.deadline;
1005
+ }
1006
+ const value = BigInt(Math.floor(params.amount * 1e6)).toString();
1007
+ const domain = {
1008
+ name: "USD Coin",
1009
+ version: "2",
1010
+ chainId: chainConfig.chainId,
1011
+ verifyingContract: chainConfig.usdc
1012
+ };
1013
+ const types = {
1014
+ Permit: [
1015
+ { name: "owner", type: "address" },
1016
+ { name: "spender", type: "address" },
1017
+ { name: "value", type: "uint256" },
1018
+ { name: "nonce", type: "uint256" },
1019
+ { name: "deadline", type: "uint256" }
1020
+ ]
1021
+ };
1022
+ const message = {
1023
+ owner: wallet.address,
1024
+ spender: params.spender,
1025
+ value,
1026
+ nonce,
1027
+ deadline
1028
+ };
1029
+ const signature = await wallet.signTypedData(domain, types, message);
1030
+ const sig = import_ethers4.ethers.Signature.from(signature);
1031
+ return {
1032
+ owner: wallet.address,
1033
+ spender: params.spender,
1034
+ value,
1035
+ deadline,
1036
+ nonce,
1037
+ v: sig.v,
1038
+ r: sig.r,
1039
+ s: sig.s
1040
+ };
1041
+ }
1042
+ var PermitSigner = class {
1043
+ config;
1044
+ constructor(config) {
1045
+ this.config = config;
1046
+ }
1047
+ async sign(params) {
1048
+ return signPermit(this.config, params);
1049
+ }
1050
+ };
1051
+
1052
+ // src/wallet/AllowanceWallet.ts
1053
+ var import_ethers5 = require("ethers");
1054
+ var PERMIT_ABI2 = [
1055
+ ...ERC20_ABI,
1056
+ "function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)",
1057
+ "function transferFrom(address from, address to, uint256 amount) returns (bool)",
1058
+ "function allowance(address owner, address spender) view returns (uint256)",
1059
+ "function nonces(address owner) view returns (uint256)"
1060
+ ];
1061
+ var AllowanceWallet = class {
1062
+ chain;
1063
+ chainConfig;
1064
+ address;
1065
+ // Agent's address
1066
+ wallet;
1067
+ provider;
1068
+ usdcContract;
1069
+ /** Stored permits from owners */
1070
+ permits = /* @__PURE__ */ new Map();
1071
+ constructor(config) {
1072
+ this.chain = config.chain || "base_sepolia";
1073
+ this.chainConfig = getChain(this.chain);
1074
+ const rpcUrl = config.rpcUrl || this.chainConfig.rpc;
1075
+ this.provider = new import_ethers5.ethers.JsonRpcProvider(rpcUrl);
1076
+ this.wallet = new import_ethers5.ethers.Wallet(config.privateKey, this.provider);
1077
+ this.address = this.wallet.address;
1078
+ this.usdcContract = new import_ethers5.ethers.Contract(
1079
+ this.chainConfig.usdc,
1080
+ PERMIT_ABI2,
1081
+ this.wallet
1082
+ );
1083
+ }
1084
+ /**
1085
+ * Store a Permit received from Owner
1086
+ * Call this when Owner sends you a signed Permit
1087
+ */
1088
+ storePermit(permit) {
1089
+ const ownerLower = permit.owner.toLowerCase();
1090
+ this.permits.set(ownerLower, permit);
1091
+ }
1092
+ /**
1093
+ * Get stored Permit for an owner
1094
+ */
1095
+ getPermit(owner) {
1096
+ return this.permits.get(owner.toLowerCase());
1097
+ }
1098
+ /**
1099
+ * Check allowance status with an owner
1100
+ */
1101
+ async checkAllowance(owner) {
1102
+ const ownerAddress = import_ethers5.ethers.getAddress(owner);
1103
+ const [allowance, ownerBalance, agentGasBalance] = await Promise.all([
1104
+ this.usdcContract.allowance(ownerAddress, this.address),
1105
+ this.usdcContract.balanceOf(ownerAddress),
1106
+ this.provider.getBalance(this.address)
1107
+ ]);
1108
+ const allowanceNum = Number(allowance) / 1e6;
1109
+ const hasGas = Number(import_ethers5.ethers.formatEther(agentGasBalance)) >= 1e-4;
1110
+ return {
1111
+ owner: ownerAddress,
1112
+ agent: this.address,
1113
+ allowance: allowanceNum.toFixed(2),
1114
+ ownerBalance: (Number(ownerBalance) / 1e6).toFixed(2),
1115
+ agentGasBalance: import_ethers5.ethers.formatEther(agentGasBalance),
1116
+ canSpend: allowanceNum > 0 && hasGas,
1117
+ chain: this.chainConfig.name
1118
+ };
1119
+ }
1120
+ /**
1121
+ * Spend from Owner's wallet using Permit allowance
1122
+ *
1123
+ * @example
1124
+ * ```typescript
1125
+ * const agent = new AllowanceWallet({
1126
+ * chain: 'base',
1127
+ * privateKey: process.env.AGENT_KEY // Only needs gas
1128
+ * });
1129
+ *
1130
+ * // Owner gave us a Permit
1131
+ * agent.storePermit(ownerPermit);
1132
+ *
1133
+ * // Spend to pay for a service
1134
+ * const result = await agent.spend({
1135
+ * to: '0xServiceProvider...',
1136
+ * amount: 2.99,
1137
+ * });
1138
+ * ```
1139
+ */
1140
+ async spend(params) {
1141
+ const { to, amount, permit } = params;
1142
+ try {
1143
+ const toAddress = import_ethers5.ethers.getAddress(to);
1144
+ const amountWei = BigInt(Math.floor(amount * 1e6));
1145
+ let ownerPermit = permit;
1146
+ let ownerAddress;
1147
+ if (ownerPermit) {
1148
+ ownerAddress = import_ethers5.ethers.getAddress(ownerPermit.owner);
1149
+ this.storePermit(ownerPermit);
1150
+ } else {
1151
+ for (const [owner, p] of this.permits) {
1152
+ const allowance = await this.usdcContract.allowance(owner, this.address);
1153
+ if (BigInt(allowance) >= amountWei) {
1154
+ ownerAddress = import_ethers5.ethers.getAddress(owner);
1155
+ ownerPermit = p;
1156
+ break;
1157
+ }
1158
+ }
1159
+ if (!ownerPermit) {
1160
+ return {
1161
+ success: false,
1162
+ error: "No valid permit found. Ask Owner to sign a Permit first.",
1163
+ from: "",
1164
+ to: toAddress,
1165
+ amount
1166
+ };
1167
+ }
1168
+ }
1169
+ const currentAllowance = await this.usdcContract.allowance(ownerAddress, this.address);
1170
+ if (BigInt(currentAllowance) < amountWei) {
1171
+ const now = Math.floor(Date.now() / 1e3);
1172
+ if (ownerPermit.deadline < now) {
1173
+ return {
1174
+ success: false,
1175
+ error: `Permit expired at ${new Date(ownerPermit.deadline * 1e3).toISOString()}. Ask Owner for a new Permit.`,
1176
+ from: ownerAddress,
1177
+ to: toAddress,
1178
+ amount
1179
+ };
1180
+ }
1181
+ const currentNonce = await this.usdcContract.nonces(ownerAddress);
1182
+ if (Number(currentNonce) !== ownerPermit.nonce) {
1183
+ return {
1184
+ success: false,
1185
+ error: `Permit nonce mismatch (expected ${ownerPermit.nonce}, got ${currentNonce}). Owner may have used this permit or signed a new one.`,
1186
+ from: ownerAddress,
1187
+ to: toAddress,
1188
+ amount
1189
+ };
1190
+ }
1191
+ console.log("[AllowanceWallet] Submitting permit on-chain...");
1192
+ const permitTx = await this.usdcContract.permit(
1193
+ ownerAddress,
1194
+ this.address,
1195
+ ownerPermit.value,
1196
+ ownerPermit.deadline,
1197
+ ownerPermit.v,
1198
+ ownerPermit.r,
1199
+ ownerPermit.s
1200
+ );
1201
+ await permitTx.wait();
1202
+ console.log("[AllowanceWallet] Permit submitted:", permitTx.hash);
1203
+ }
1204
+ console.log("[AllowanceWallet] Executing transferFrom...");
1205
+ const tx = await this.usdcContract.transferFrom(
1206
+ ownerAddress,
1207
+ toAddress,
1208
+ amountWei
1209
+ );
1210
+ const receipt = await tx.wait();
1211
+ const newAllowance = await this.usdcContract.allowance(ownerAddress, this.address);
1212
+ return {
1213
+ success: true,
1214
+ txHash: tx.hash,
1215
+ from: ownerAddress,
1216
+ to: toAddress,
1217
+ amount,
1218
+ remainingAllowance: (Number(newAllowance) / 1e6).toFixed(2),
1219
+ explorerUrl: `${this.chainConfig.explorerTx}${tx.hash}`
1220
+ };
1221
+ } catch (error) {
1222
+ const message = error.message;
1223
+ if (message.includes("ERC20InsufficientAllowance")) {
1224
+ return {
1225
+ success: false,
1226
+ error: "Insufficient allowance. Ask Owner to sign a new Permit with higher amount.",
1227
+ from: "",
1228
+ to,
1229
+ amount
1230
+ };
1231
+ }
1232
+ if (message.includes("ERC20InsufficientBalance")) {
1233
+ return {
1234
+ success: false,
1235
+ error: "Owner's wallet has insufficient USDC balance.",
1236
+ from: "",
1237
+ to,
1238
+ amount
1239
+ };
1240
+ }
1241
+ return {
1242
+ success: false,
1243
+ error: message,
1244
+ from: "",
1245
+ to,
1246
+ amount
1247
+ };
1248
+ }
1249
+ }
1250
+ /**
1251
+ * Get Agent's gas balance (ETH)
1252
+ */
1253
+ async getGasBalance() {
1254
+ const balance = await this.provider.getBalance(this.address);
1255
+ return import_ethers5.ethers.formatEther(balance);
1256
+ }
1257
+ };
1258
+ function generatePermitInstructions(params) {
1259
+ const { ownerAddress, agentAddress, amount, deadlineHours = 24, chain = "base" } = params;
1260
+ const chainConfig = getChain(chain);
1261
+ const deadline = Math.floor(Date.now() / 1e3) + deadlineHours * 3600;
1262
+ const value = BigInt(Math.floor(amount * 1e6)).toString();
1263
+ const eip712Domain = {
1264
+ name: "USD Coin",
1265
+ version: "2",
1266
+ chainId: chainConfig.chainId,
1267
+ verifyingContract: chainConfig.usdc
1268
+ };
1269
+ const typedData = {
1270
+ types: {
1271
+ EIP712Domain: [
1272
+ { name: "name", type: "string" },
1273
+ { name: "version", type: "string" },
1274
+ { name: "chainId", type: "uint256" },
1275
+ { name: "verifyingContract", type: "address" }
1276
+ ],
1277
+ Permit: [
1278
+ { name: "owner", type: "address" },
1279
+ { name: "spender", type: "address" },
1280
+ { name: "value", type: "uint256" },
1281
+ { name: "nonce", type: "uint256" },
1282
+ { name: "deadline", type: "uint256" }
1283
+ ]
1284
+ },
1285
+ primaryType: "Permit",
1286
+ domain: eip712Domain,
1287
+ message: {
1288
+ owner: ownerAddress,
1289
+ spender: agentAddress,
1290
+ value,
1291
+ nonce: "<GET_FROM_USDC_CONTRACT>",
1292
+ // Owner needs to query this
1293
+ deadline
1294
+ }
1295
+ };
1296
+ const instructions = `
1297
+ \u{1F510} **Grant USDC Spending Allowance to Your Agent**
1298
+
1299
+ Your Agent (${agentAddress}) is requesting permission to spend up to ${amount} USDC from your wallet.
1300
+
1301
+ **What this does:**
1302
+ - Allows your Agent to pay for services on your behalf
1303
+ - Your USDC stays in YOUR wallet until spent
1304
+ - Agent can only spend up to the authorized amount
1305
+ - Expires in ${deadlineHours} hours
1306
+ - You can revoke anytime by moving your USDC
1307
+
1308
+ **How to sign (MetaMask / any web3 wallet):**
1309
+
1310
+ 1. Go to https://etherscan.io/address/${chainConfig.usdc}#readContract
1311
+ 2. Query \`nonces(${ownerAddress})\` to get your current nonce
1312
+ 3. Use eth_signTypedData_v4 with the data below (replace nonce)
1313
+ 4. Send the signature {v, r, s, deadline, nonce} to your Agent
1314
+
1315
+ **Chain:** ${chainConfig.name}
1316
+ **USDC Contract:** ${chainConfig.usdc}
1317
+
1318
+ **EIP-712 Typed Data:**
1319
+ \`\`\`json
1320
+ ${JSON.stringify(typedData, null, 2)}
1321
+ \`\`\`
1322
+
1323
+ \u26A0\uFE0F Never share your private key. This signature only authorizes spending, not wallet access.
1324
+ `;
1325
+ return { instructions, typedData, eip712Domain };
1326
+ }
979
1327
  // Annotate the CommonJS export names for ESM import in node:
980
1328
  0 && (module.exports = {
1329
+ AllowanceWallet,
1330
+ PermitSigner,
981
1331
  PermitWallet,
982
1332
  SecureWallet,
983
1333
  Wallet,
984
1334
  createWallet,
985
1335
  formatPermitRequest,
1336
+ generatePermitInstructions,
986
1337
  getWalletAddress,
987
1338
  loadWallet,
1339
+ signPermit,
988
1340
  walletExists
989
1341
  });
990
1342
  //# sourceMappingURL=index.js.map