diff --git a/examples/package.json b/examples/package.json index 265ec4fdd..ee25576ca 100644 --- a/examples/package.json +++ b/examples/package.json @@ -39,7 +39,8 @@ "@types/express": "^4.17.21", "commander": "^12.0.0", "express": "^4.21.1", - "tsx": "^4.19.1" + "tsx": "^4.19.1", + "viem": "^2.21.1" }, "devDependencies": { "@types/node": "20", diff --git a/examples/rental-model/advanced/deposit/config.ts b/examples/rental-model/advanced/deposit/config.ts new file mode 100644 index 000000000..62ae6cd3b --- /dev/null +++ b/examples/rental-model/advanced/deposit/config.ts @@ -0,0 +1,14 @@ +export default { + funder: { + address: "0x00", + privateKey: "0x00", + nonceSpace: 1000000, + }, + rpcUrl: "https://holesky.rpc-node.dev.golem.network", + lockPaymentContract: { + holeskyAddress: "0x63704675f72A47a7a183112700Cb48d4B0A94332", + }, + glmContract: { + holeskyAddress: "0x8888888815bf4DB87e57B609A50f938311EEd068", + }, +}; diff --git a/examples/rental-model/advanced/deposit/contracts/glmAbi.json b/examples/rental-model/advanced/deposit/contracts/glmAbi.json new file mode 100644 index 000000000..68f507842 --- /dev/null +++ b/examples/rental-model/advanced/deposit/contracts/glmAbi.json @@ -0,0 +1,470 @@ +[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "spender", + "type": "address" + }, + { + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "sender", + "type": "address" + }, + { + "name": "recipient", + "type": "address" + }, + { + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "PERMIT_TYPEHASH", + "outputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "spender", + "type": "address" + }, + { + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "account", + "type": "address" + }, + { + "name": "amount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "version", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "holder", + "type": "address" + }, + { + "name": "spender", + "type": "address" + }, + { + "name": "nonce", + "type": "uint256" + }, + { + "name": "expiry", + "type": "uint256" + }, + { + "name": "allowed", + "type": "bool" + }, + { + "name": "v", + "type": "uint8" + }, + { + "name": "r", + "type": "bytes32" + }, + { + "name": "s", + "type": "bytes32" + } + ], + "name": "permit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "account", + "type": "address" + } + ], + "name": "addMinter", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "renounceMinter", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "spender", + "type": "address" + }, + { + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "recipient", + "type": "address" + }, + { + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "account", + "type": "address" + } + ], + "name": "isMinter", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "owner", + "type": "address" + }, + { + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "name": "_migrationAgent", + "type": "address" + }, + { + "name": "_chainId", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "account", + "type": "address" + } + ], + "name": "MinterAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "account", + "type": "address" + } + ], + "name": "MinterRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "from", + "type": "address" + }, + { + "indexed": true, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + } +] diff --git a/examples/rental-model/advanced/deposit/contracts/lockAbi.json b/examples/rental-model/advanced/deposit/contracts/lockAbi.json new file mode 100644 index 000000000..bbe8108d4 --- /dev/null +++ b/examples/rental-model/advanced/deposit/contracts/lockAbi.json @@ -0,0 +1,631 @@ +[ + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "_GLM", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "DepositClosed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "DepositCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "DepositExtended", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "amount", + "type": "uint128" + } + ], + "name": "DepositFeeTransfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "DepositTerminated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "amount", + "type": "uint128" + } + ], + "name": "DepositTransfer", + "type": "event" + }, + { + "inputs": [], + "name": "CONTRACT_ID", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "CONTRACT_ID_AND_VERSION", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "CONTRACT_VERSION", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "GLM", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "closeDeposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint128", + "name": "amount", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "flatFeeAmount", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "validToTimestamp", + "type": "uint64" + } + ], + "name": "createDeposit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "addr", + "type": "address" + }, + { + "internalType": "uint128", + "name": "amount", + "type": "uint128" + } + ], + "name": "depositSingleTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "addr", + "type": "address" + }, + { + "internalType": "uint128", + "name": "amount", + "type": "uint128" + } + ], + "name": "depositSingleTransferAndClose", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "bytes32[]", + "name": "payments", + "type": "bytes32[]" + } + ], + "name": "depositTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "bytes32[]", + "name": "payments", + "type": "bytes32[]" + } + ], + "name": "depositTransferAndClose", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "deposits", + "outputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint64", + "name": "validTo", + "type": "uint64" + }, + { + "internalType": "uint128", + "name": "amount", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "feeAmount", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + }, + { + "internalType": "uint128", + "name": "additionalAmount", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "additionalFlatFee", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "validToTimestamp", + "type": "uint64" + } + ], + "name": "extendDeposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "funderFromId", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "getDeposit", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + }, + { + "internalType": "address", + "name": "funder", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint128", + "name": "amount", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "validTo", + "type": "uint64" + } + ], + "internalType": "struct ILockPayment.DepositView", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + }, + { + "internalType": "address", + "name": "funder", + "type": "address" + } + ], + "name": "getDepositByNonce", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + }, + { + "internalType": "address", + "name": "funder", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint128", + "name": "amount", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "validTo", + "type": "uint64" + } + ], + "internalType": "struct ILockPayment.DepositView", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getValidateDepositSignature", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + } + ], + "name": "idFromNonce", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + }, + { + "internalType": "address", + "name": "funder", + "type": "address" + } + ], + "name": "idFromNonceAndFunder", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "nonceFromId", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + } + ], + "name": "terminateDeposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint128", + "name": "flatFeeAmount", + "type": "uint128" + } + ], + "name": "validateDeposit", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/examples/rental-model/advanced/deposit/funder.ts b/examples/rental-model/advanced/deposit/funder.ts new file mode 100644 index 000000000..61baa0b40 --- /dev/null +++ b/examples/rental-model/advanced/deposit/funder.ts @@ -0,0 +1,218 @@ +import { Address, createPublicClient, createWalletClient, formatEther, Hex, http, parseEther } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { holesky } from "viem/chains"; +import chalk from "chalk"; +import { readFile } from "fs/promises"; +import config from "./config.js"; + +const abiGlm = JSON.parse(await readFile("./contracts/glmAbi.json", "utf8")); +const abiLock = JSON.parse(await readFile("./contracts/lockAbi.json", "utf8")); +const funderAccount = privateKeyToAccount(config.funder.privateKey); +const nonce = Math.floor(Math.random() * config.funder.nonceSpace); + +// walletClient for writeContract functions +const walletClient = createWalletClient({ + account: funderAccount, + chain: holesky, + transport: http(config.rpcUrl), +}); + +// publicClient for readContract functions +const publicClient = createPublicClient({ + chain: holesky, + transport: http(config.rpcUrl), +}); + +const LOCK_CONTRACT = { + address: config.lockPaymentContract.holeskyAddress, + abi: abiLock, +}; +const GLM_CONTRACT = { + address: config.glmContract.holeskyAddress, + abi: abiGlm, +}; + +async function createAllowance({ budget, fee }: { budget: number; fee: number }) { + const amountWei = parseEther(`${budget}`); + const flatFeeAmountWei = parseEther(`${fee}`); + const allowanceBudget = amountWei + flatFeeAmountWei; + + console.log( + chalk.yellow( + `\nCreating allowance of ${formatEther(allowanceBudget)} GLM for ${LOCK_CONTRACT.address} contract ...`, + ), + ); + + const hash = await walletClient.writeContract({ + address: GLM_CONTRACT.address, + abi: GLM_CONTRACT.abi, + functionName: "increaseAllowance", + args: [LOCK_CONTRACT.address, allowanceBudget], + chain: walletClient.chain, + account: walletClient.account, + }); + + const receipt = await publicClient.waitForTransactionReceipt({ + hash, + }); + + console.log(chalk.yellow(`Allowance successfully created with Tx ${receipt.transactionHash}.`)); +} + +async function checkAllowance() { + const args = [config.funder.address, LOCK_CONTRACT.address]; + + console.log(chalk.yellow(`\nChecking allowance for ${args[1]} contract ...`)); + + const allowance = await publicClient.readContract({ + abi: GLM_CONTRACT.abi, + functionName: "allowance", + address: GLM_CONTRACT.address, + args, + }); + + console.log(chalk.yellow(`Allowance of ${formatEther(allowance)} GLM is set.`)); +} + +type DepositParams = { + address: string; + budget: number; + fee: number; + expirationSec: number; +}; + +async function createDeposit({ address, budget, fee, expirationSec }: DepositParams) { + const validToTimestamp = new Date().getTime() + expirationSec * 1000; + const args = [BigInt(nonce), address, parseEther(`${budget}`), parseEther(`${fee}`), BigInt(validToTimestamp)]; + + console.log( + chalk.blueBright( + `\nCreating deposit of amount: ${formatEther(args[2])} GLM, flatFeeAmount: ${formatEther(args[3])} GLM, for ${(expirationSec / 3600).toFixed(2)} hours.`, + ), + ); + console.log(chalk.blueBright(`Using contract at address: ${LOCK_CONTRACT.address}.`)); + + const hash = await walletClient.writeContract({ + address: LOCK_CONTRACT.address, + abi: LOCK_CONTRACT.abi, + functionName: "createDeposit", + args, + chain: walletClient.chain, + account: walletClient.account, + }); + + await publicClient.waitForTransactionReceipt({ + hash, + }); + + console.log(chalk.blueBright(`Deposit successfully created with Tx ${hash}.`)); + + const depositId = await getDepositID(); + const depositDetails = await getDepositDetails(); + return { + id: "0x" + depositId.toString(16), + amount: depositDetails.amount, + contract: config.lockPaymentContract.holeskyAddress, + }; +} + +async function extendDeposit({ budget, fee, expirationSec }: Partial & { expirationSec: number }) { + const validToTimestamp = new Date().getTime() + expirationSec * 1000; + const args = [ + BigInt(nonce), + BigInt(budget ? parseEther(`${budget}`) : 0), + BigInt(fee ? parseEther(`${fee}`) : 0), + BigInt(validToTimestamp), + ]; + + console.log( + chalk.blueBright( + `\nExtending deposit of additional amount: ${formatEther(args[1])} GLM, flatFeeAmount: ${formatEther(args[2])} GLM, for ${(expirationSec / 3600).toFixed(2)} hours.`, + ), + ); + console.log(chalk.blueBright(`Using contract at address: ${LOCK_CONTRACT.address}.`)); + + const hash = await walletClient.writeContract({ + abi: LOCK_CONTRACT.abi, + functionName: "extendDeposit", + address:
LOCK_CONTRACT.address, + args, + chain: walletClient.chain, + account: walletClient.account, + }); + + await publicClient.waitForTransactionReceipt({ + hash, + }); + + console.log(chalk.blueBright(`Deposit successfully extended with Tx ${hash}.`)); +} + +async function getDepositID() { + const depositID = await publicClient.readContract({ + address:
LOCK_CONTRACT.address, + abi: LOCK_CONTRACT.abi, + functionName: "idFromNonceAndFunder", + args: [BigInt(nonce), config.funder.address], + }); + + console.log( + chalk.blueBright(`\nDepositID: ${depositID} available on contract at address: ${LOCK_CONTRACT.address}.`), + ); + return depositID; +} + +interface DepositData { + amount: bigint; + id: string; +} + +async function getDepositDetails() { + const deposit = await publicClient.readContract({ + address:
LOCK_CONTRACT.address, + abi: LOCK_CONTRACT.abi, + functionName: "getDepositByNonce", + args: [BigInt(nonce), config.funder.address], + }); + + console.log( + chalk.blueBright(`\nDeposit of `), + deposit, + chalk.grey(` available on contract ${LOCK_CONTRACT.address}.`), + ); + const depositData = { + amount: formatEther(deposit.amount), + id: deposit.id.toString(), + contract: LOCK_CONTRACT.address, + }; + return depositData; +} + +async function clearAllowance() { + const args = [LOCK_CONTRACT.address, BigInt(0)]; + + console.log(chalk.yellow(`\nClearing allowance for ${args[0]} contract ...`)); + + const hash = await walletClient.writeContract({ + abi: GLM_CONTRACT.abi, + functionName: "approve", + address:
GLM_CONTRACT.address, + args, + chain: walletClient.chain, + account: walletClient.account, + }); + + await publicClient.waitForTransactionReceipt({ + hash, + }); + + console.log(chalk.yellow(`Allowance cleared with Tx ${hash}.\n`)); +} + +export default { + createAllowance, + checkAllowance, + createDeposit, + extendDeposit, + clearAllowance, +}; diff --git a/examples/rental-model/advanced/deposit/index.ts b/examples/rental-model/advanced/deposit/index.ts new file mode 100644 index 000000000..ab1b7dd30 --- /dev/null +++ b/examples/rental-model/advanced/deposit/index.ts @@ -0,0 +1,116 @@ +/** + * In this example we demonstrate executing tasks on a golem but using funds deposited by another person. + * It is called Funder. The funder is responsible for allocating the deposit, + * which will then be used by the Spender (requestor) to create an allocation for a payment. + * + * To run the example, it is necessary to define the funder's address in the config.ts file and a private key + * that will allow depositing specific funds on the contract. + * + * In order to check if everything went correctly, the Observer logs transaction information + * in the smart contract and the script waits for confirmation on the blockchain until the deposit is closed. + * + * IMPORTANT: this feature is only supported with yagna versions >= 0.16.0 + */ + +import funder from "./funder"; +import observer from "./observer"; +import { GolemNetwork, MarketOrderSpec, waitFor } from "@golem-sdk/golem-js"; +import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; + +(async () => { + const glm = new GolemNetwork({ + logger: pinoPrettyLogger({ level: "info" }), + }); + + try { + await glm.connect(); + + const { identity: spenderAddress } = await glm.services.yagna.identity.getIdentity(); + const budget = 1.0; + const fee = 0.5; + const expirationSec = 60 * 60; // 1 hour + + // In order for a founder to create a deposit, he must first create an allowance. + await funder.createAllowance({ budget, fee }); + // Check if allowance was created correctly + await funder.checkAllowance(); + + const deposit = await funder.createDeposit({ address: spenderAddress, budget, fee, expirationSec }); + // The funder can also extend the deposit, for example by extending expiration time + await funder.extendDeposit({ expirationSec: expirationSec + 5 * 60 }); // extend time by 5 min + + // After the deposit is properly prepared, the funder can clear the allowance + await funder.clearAllowance(); + + // Now the Spender (requestor) can use the deposit to create an allocation + const allocation = await glm.payment.createAllocation({ + deposit, + budget, + expirationSec, + }); + + // We are starting the contract transaction observations for the spender address + const observation = await observer.startWatchingContractTransactions(spenderAddress); + + const order1: MarketOrderSpec = { + demand: { + workload: { imageTag: "golem/alpine:latest" }, + }, + market: { + rentHours: 0.5, + pricing: { + model: "burn-rate", + avgGlmPerHour: 0.5, + }, + }, + payment: { + allocation, + }, + }; + + const order2: MarketOrderSpec = { + demand: { + workload: { imageTag: "golem/alpine:latest" }, + }, + market: { + rentHours: 0.5, + pricing: { + model: "burn-rate", + avgGlmPerHour: 0.5, + }, + }, + payment: { + allocation: allocation.id, // alternative way to pass allocation ID + }, + }; + + const rental1 = await glm.oneOf({ order: order1 }); + + await rental1 + .getExeUnit() + .then((exe) => exe.run(`echo Task 1 running on provider ${exe.provider.name} 👽`)) + .then((res) => console.log(res.stdout)); + + await rental1.stopAndFinalize(); + + const rental2 = await glm.oneOf({ order: order2 }); + + await rental2 + .getExeUnit() + .then((exe) => exe.run(`echo Task 2 Running on provider ${exe.provider.name} 🤠`)) + .then((res) => console.log(res.stdout)); + + await rental2.stopAndFinalize(); + + // Once Spender releases the allocation, the deposit will be closed and cannot be used again. + await glm.payment.releaseAllocation(allocation); + + // We wait (max 2 mins) for confirmation from blockchain + await waitFor(observation.isDepositClosed, { abortSignal: AbortSignal.timeout(120_000) }); + observation.stopWatchingContractTransactions(); + } catch (err) { + console.error("Failed to run the example", err); + } finally { + await glm.disconnect(); + } +})().catch(console.error); diff --git a/examples/rental-model/advanced/deposit/observer.ts b/examples/rental-model/advanced/deposit/observer.ts new file mode 100644 index 000000000..2af2d3871 --- /dev/null +++ b/examples/rental-model/advanced/deposit/observer.ts @@ -0,0 +1,68 @@ +import { Address, createPublicClient, decodeFunctionData, Hex, http, Log, parseAbi } from "viem"; +import config from "./config.js"; +import { holesky } from "viem/chains"; +import { readFile } from "fs/promises"; +import chalk from "chalk"; + +const abiLock = JSON.parse(await readFile("./contracts/lockAbi.json", "utf8")); + +const publicClient = createPublicClient({ + chain: holesky, + transport: http(config.rpcUrl), +}); + +async function checkIsDopositClosed(spenderAddress: Address, funderAddress: Address, logs: Log[]) { + for (const log of logs) { + if (!log.transactionHash) return false; + const transaction = await publicClient.getTransaction({ hash: log.transactionHash }); + const parsedMethod = decodeFunctionData({ + abi: abiLock, + data: transaction.input, + }); + const functionNameWithArgs = `${parsedMethod.functionName}(${parsedMethod.args.join(",")})`; + console.log(chalk.magenta("\nContract transaction log:")); + console.log(chalk.magenta("call:"), functionNameWithArgs); + console.log(chalk.magenta("event:"), "eventName" in log ? log["eventName"] : ""); + console.log(chalk.magenta("from:"), transaction.from); + console.log(chalk.magenta("hash:"), transaction.hash, "\n"); + const functionName = parsedMethod.functionName.toLowerCase(); + // if deposit is closed by spender (requestor) + if (functionName.includes("close") && transaction.from === spenderAddress) { + console.log(chalk.blueBright(`Deposit has been closed by spender.`)); + return true; + } + // if deposit is terminated by funder + if (functionName === "terminatedeposit" && transaction.from === funderAddress) { + console.log(chalk.blueBright(`Deposit has been terminated by spender.`)); + return true; + } + } + return false; +} + +async function startWatchingContractTransactions(address: string) { + const spenderAddress =
address; + const funderAddress =
config.funder.address; + let isDepositClosed = false; + + const unwatch = publicClient.watchEvent({ + onLogs: async (logs) => (isDepositClosed = await checkIsDopositClosed(spenderAddress, funderAddress, logs)), + events: parseAbi([ + "event DepositCreated(uint256 indexed id, address spender)", + "event DepositClosed(uint256 indexed id, address spender)", + "event DepositExtended(uint256 indexed id, address spender)", + "event DepositFeeTransfer(uint256 indexed id, address spender, uint128 amount)", + "event DepositTerminated(uint256 indexed id, address spender)", + "event DepositTransfer(uint256 indexed id, address spender, address recipient, uint128 amount)", + ]), + address:
config.lockPaymentContract.holeskyAddress, + }); + return { + stopWatchingContractTransactions: unwatch, + isDepositClosed: () => isDepositClosed, + }; +} + +export default { + startWatchingContractTransactions, +}; diff --git a/examples/rental-model/advanced/outbound/whitelist/manifest.json b/examples/rental-model/advanced/outbound/whitelist/manifest.json new file mode 100644 index 000000000..7f2804dd5 --- /dev/null +++ b/examples/rental-model/advanced/outbound/whitelist/manifest.json @@ -0,0 +1,32 @@ +{ + "version": "0.1.0", + "createdAt": "2024-08-21T21:55:37.123+02:00", + "expiresAt": "2024-11-19T21:55:37.123+01:00", + "metadata": { + "name": "outbound-example-project", + "version": "1.0.0" + }, + "payload": [ + { + "platform": { + "os": "linux", + "arch": "x86_64" + }, + "hash": "sha3:f985714e913cf2f448c8dac86b3b4a82d3f2e7ece490e24428d6c675", + "urls": [ + "http://registry.golem.network/download/656b365b59fb63da42918f861a1ddba85c61223021efd4cc9ef0c017089ac0df" + ] + } + ], + "compManifest": { + "version": "0.1.0", + "net": { + "inet": { + "out": { + "urls": ["https://registry.npmjs.org"], + "protocols": ["https"] + } + } + } + } +} diff --git a/examples/rental-model/advanced/outbound/whitelist/read-golem-js-releases.ts b/examples/rental-model/advanced/outbound/whitelist/read-golem-js-releases.ts new file mode 100644 index 000000000..4ad74ac22 --- /dev/null +++ b/examples/rental-model/advanced/outbound/whitelist/read-golem-js-releases.ts @@ -0,0 +1,88 @@ +/** + * Whitelist Outbound Internet Access Example + * + * This example presents how you can leverage the whitelist outbound rule. + * + * Few things to keep in mind: + * + * - your Requestor script has to present a manifest (manifest.json) as part of the demand when negotiating + * with Providers + * - the Providers need to have the URL which you're trying to reach on their whitelist, if the address + * which you're using is not on the pre-installed one (which the providers can clear if they want to) + * then you need to reach out to the Provider community (you can use Golem Network's Discord) and + * request opening some URL by them for you : + * + * @link https://github.com/golemfactory/ya-installer-resources/tree/main/whitelist Pre-installed whitelist + * + * This example reaches out to the whitelisted registry.npmjs.org to download golem-js release information + * + * WORKING WITH THE MANIFEST + * + * `@golem-sdk/cli` to the rescue - check the `manifest` sub-command to generate the manifest for your + * requestor and maintain the outbound configuration + * + */ +import { GolemNetwork } from "@golem-sdk/golem-js"; +import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "url"; + +const dirName = path.dirname(fileURLToPath(import.meta.url)); + +(async () => { + const logger = pinoPrettyLogger({ + level: "info", + }); + + const glm = new GolemNetwork({ + logger, + }); + + try { + await glm.connect(); + + /** + * Used to terminate the script after 60s in any case + * + * It's possible that no provider will be accepting your offer due to not having the base URL + * which you are trying to reach on their whitelist, or they don't allow whitelist outbound + * access at all. + */ + const timeoutSignal = AbortSignal.timeout(120_000); + const onTimeout = () => console.log("Reached timeout, no one wanted to collaborate"); + timeoutSignal.addEventListener("abort", onTimeout); + + const rental = await glm.oneOf({ + order: { + demand: { + workload: { + imageTag: "golem/node:latest", + manifest: fs.readFileSync(path.join(dirName, "manifest.json")).toString("base64"), + }, + }, + market: { + rentHours: 15 / 60, + pricing: { + model: "burn-rate", + avgGlmPerHour: 1, + }, + }, + }, + signalOrTimeout: timeoutSignal, + }); + + timeoutSignal.removeEventListener("abort", onTimeout); + + const exe = await rental.getExeUnit(); + const result = await exe.run("curl https://registry.npmjs.org/-/package/@golem-sdk/golem-js/dist-tags"); + + console.log("golem-js release tags:", result.getOutputAsJson()); + + await rental.stopAndFinalize(); + } catch (err) { + console.error(err); + } finally { + await glm.disconnect(); + } +})().catch(console.error); diff --git a/examples/tsconfig.json b/examples/tsconfig.json index b09c90abe..019ecf870 100644 --- a/examples/tsconfig.json +++ b/examples/tsconfig.json @@ -3,7 +3,7 @@ "module": "esnext", "target": "esnext", "strict": true, - "noImplicitAny": false, + "noImplicitAny": true, "esModuleInterop": true, "moduleResolution": "Bundler", "removeComments": true, diff --git a/package-lock.json b/package-lock.json index 570e3eda4..7df1a1568 100644 --- a/package-lock.json +++ b/package-lock.json @@ -89,6 +89,7 @@ } }, "examples": { + "name": "golem-js-examples", "version": "0.0.0", "license": "LGPL-3.0", "dependencies": { @@ -97,7 +98,8 @@ "@types/express": "^4.17.21", "commander": "^12.0.0", "express": "^4.21.1", - "tsx": "^4.19.1" + "tsx": "^4.19.1", + "viem": "^2.21.1" }, "devDependencies": { "@types/node": "20", @@ -132,6 +134,11 @@ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz", + "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -2472,6 +2479,31 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@noble/curves": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.6.0.tgz", + "integrity": "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==", + "dependencies": { + "@noble/hashes": "1.5.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", + "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3200,6 +3232,39 @@ "win32" ] }, + "node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.5.0.tgz", + "integrity": "sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw==", + "dependencies": { + "@noble/curves": "~1.6.0", + "@noble/hashes": "~1.5.0", + "@scure/base": "~1.1.7" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.4.0.tgz", + "integrity": "sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw==", + "dependencies": { + "@noble/hashes": "~1.5.0", + "@scure/base": "~1.1.8" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -4626,6 +4691,26 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, + "node_modules/abitype": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.6.tgz", + "integrity": "sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A==", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3 >=3.22.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -9505,6 +9590,20 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/isows": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", + "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "peerDependencies": { + "ws": "*" + } + }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -20038,7 +20137,7 @@ "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -20325,6 +20424,36 @@ "extsprintf": "^1.2.0" } }, + "node_modules/viem": { + "version": "2.21.25", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.21.25.tgz", + "integrity": "sha512-fQbFLVW5RjC1MwjelmzzDygmc2qMfY17NruAIIdYeiB8diQfhqsczU5zdGw/jTbmNXbKoYnSdgqMb8MFZcbZ1w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "1.11.0", + "@noble/curves": "1.6.0", + "@noble/hashes": "1.5.0", + "@scure/bip32": "1.5.0", + "@scure/bip39": "1.4.0", + "abitype": "1.0.6", + "isows": "1.0.6", + "webauthn-p256": "0.0.10", + "ws": "8.18.0" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/vscode-oniguruma": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", @@ -20360,6 +20489,21 @@ "node": ">=10.13.0" } }, + "node_modules/webauthn-p256": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/webauthn-p256/-/webauthn-p256-0.0.10.tgz", + "integrity": "sha512-EeYD+gmIT80YkSIDb2iWq0lq2zbHo1CxHlQTeJ+KkCILWpVy3zASH3ByD4bopzfk0uCwXxLqKGLqp2W4O28VFA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "dependencies": { + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0" + } + }, "node_modules/webpack": { "version": "5.95.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz",