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