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.
- package/dist/cli.js +16 -3
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +16 -3
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +65 -1
- package/dist/index.d.ts +65 -1
- package/dist/index.js +466 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +462 -9
- package/dist/index.mjs.map +1 -1
- package/dist/wallet/index.d.mts +200 -1
- package/dist/wallet/index.d.ts +200 -1
- package/dist/wallet/index.js +352 -0
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs +348 -0
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/wallet/index.mjs
CHANGED
|
@@ -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
|