diff --git a/knip.ts b/knip.ts index c9897e6d..7f18129a 100644 --- a/knip.ts +++ b/knip.ts @@ -11,6 +11,7 @@ const config: KnipConfig = { 'src/lib/objects/hello-world/index.ts', 'src/lib/objects/external/iframe.svelte', 'src/lib/objects/split/types.ts', + 'src/lib/objects/split/contracts/**/*', ], paths: { // This ain't pretty, but Svelte basically does the same @@ -28,6 +29,7 @@ const config: KnipConfig = { }, ignoreExportsUsedInFile: true, ignoreBinaries: ['docker'], + ignoreDependencies: ['@typechain/ethers-v6'], } export default config diff --git a/package.json b/package.json index 2f5c3778..3c8b2968 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "knip": "knip", "start:blockchain": "hardhat node", "cli": "tsx --tsconfig ./.svelte-kit/tsconfig.json --no-warnings ./src/cli/cli.ts", + "typechain": "typechain --target ethers-v6 --out-dir ./src/lib/objects/split/contracts/types ./src/lib/objects/split/contracts/abis/*.json", "waku:start": "docker compose -f ./docker-compose.yaml up -d", "waku:stop": "docker kill waku-objects-playground-waku-1" }, @@ -24,6 +25,7 @@ "@sveltejs/adapter-auto": "^2.1.0", "@sveltejs/kit": "^1.21.0", "@total-typescript/ts-reset": "^0.4.2", + "@typechain/ethers-v6": "^0.5.0", "@types/node": "^20.5.4", "@typescript-eslint/eslint-plugin": "^5.61.0", "@typescript-eslint/parser": "^5.61.0", @@ -49,6 +51,7 @@ "svelte-check": "^3.4.4", "svelte-preprocess": "^5.0.4", "tsx": "^3.12.8", + "typechain": "^8.3.1", "typescript": "^5.1.6", "vite": "^4.3.9", "vitest": "^0.32.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 645b5d7c..99bae02c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: false + excludeLinksFromLockfile: false + devDependencies: '@bonosoft/sveltekit-qrcode': specifier: ^0.0.3 @@ -19,6 +23,9 @@ devDependencies: '@total-typescript/ts-reset': specifier: ^0.4.2 version: 0.4.2 + '@typechain/ethers-v6': + specifier: ^0.5.0 + version: 0.5.0(ethers@6.6.4)(typechain@8.3.1)(typescript@5.1.6) '@types/node': specifier: ^20.5.4 version: 20.5.4 @@ -94,6 +101,9 @@ devDependencies: tsx: specifier: ^3.12.8 version: 3.12.8 + typechain: + specifier: ^8.3.1 + version: 8.3.1(typescript@5.1.6) typescript: specifier: ^5.1.6 version: 5.1.6 @@ -2427,6 +2437,20 @@ packages: resolution: {integrity: sha512-vqd7ZUDSrXFVT1n8b2kc3LnklncDQFPvR58yUS1kEP23/nHPAO9l1lMjUfnPrXYYk4Hj54rrLKMW5ipwk7k09A==} dev: true + /@typechain/ethers-v6@0.5.0(ethers@6.6.4)(typechain@8.3.1)(typescript@5.1.6): + resolution: {integrity: sha512-wsz7AvbY5n2uVwpS2RHDYsW6wYOrhWxeTLFpxuzhO62w/ZDQEVIipArX731KA/hdqygP2zJ2RTkVXgzU1WrU1g==} + peerDependencies: + ethers: 6.x + typechain: ^8.3.1 + typescript: '>=4.7.0' + dependencies: + ethers: 6.6.4 + lodash: 4.17.21 + ts-essentials: 7.0.3(typescript@5.1.6) + typechain: 8.3.1(typescript@5.1.6) + typescript: 5.1.6 + dev: true + /@types/bn.js@4.11.6: resolution: {integrity: sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==} dependencies: @@ -2487,6 +2511,10 @@ packages: '@types/node': 20.5.4 dev: true + /@types/prettier@2.7.3: + resolution: {integrity: sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==} + dev: true + /@types/pug@2.0.6: resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==} dev: true @@ -2999,6 +3027,16 @@ packages: resolution: {integrity: sha512-fExL2kFDC1Q2DUOx3whE/9KoN66IzkY4b4zUHUBFM1ojEYjZZYDcUW3bek/ufGionX9giIKDC5redH2IlGqcQQ==} dev: true + /array-back@3.1.0: + resolution: {integrity: sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==} + engines: {node: '>=6'} + dev: true + + /array-back@4.0.2: + resolution: {integrity: sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==} + engines: {node: '>=8'} + dev: true + /array-last@1.3.0: resolution: {integrity: sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==} engines: {node: '>=0.10.0'} @@ -3354,6 +3392,26 @@ packages: resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==} dev: true + /command-line-args@5.2.1: + resolution: {integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==} + engines: {node: '>=4.0.0'} + dependencies: + array-back: 3.1.0 + find-replace: 3.0.0 + lodash.camelcase: 4.3.0 + typical: 4.0.0 + dev: true + + /command-line-usage@6.1.3: + resolution: {integrity: sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==} + engines: {node: '>=8.0.0'} + dependencies: + array-back: 4.0.2 + chalk: 2.4.2 + table-layout: 1.0.2 + typical: 5.2.0 + dev: true + /commander@3.0.2: resolution: {integrity: sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==} dev: true @@ -3485,6 +3543,11 @@ packages: type-detect: 4.0.8 dev: true + /deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: true + /deep-freeze@0.0.1: resolution: {integrity: sha512-Z+z8HiAvsGwmjqlphnHW5oz6yWlOwu6EQfFTjmeTWlDeda3FS2yv3jhq35TX/ewmsnqB+RX2IdsIOyjJCQN5tg==} dev: true @@ -4089,6 +4152,13 @@ packages: engines: {node: '>=0.10.0'} dev: true + /find-replace@3.0.0: + resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==} + engines: {node: '>=4.0.0'} + dependencies: + array-back: 3.1.0 + dev: true + /find-up@2.1.0: resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} engines: {node: '>=4'} @@ -4239,6 +4309,17 @@ packages: path-scurry: 1.10.1 dev: true + /glob@7.1.7: + resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + /glob@7.2.0: resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} dependencies: @@ -5225,6 +5306,10 @@ packages: p-locate: 5.0.0 dev: true + /lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + dev: true + /lodash.curry@4.1.1: resolution: {integrity: sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA==} dev: true @@ -5436,6 +5521,12 @@ packages: minimist: 1.2.8 dev: true + /mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + dev: true + /mlly@1.4.0: resolution: {integrity: sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==} dependencies: @@ -6054,6 +6145,11 @@ packages: ms: 2.1.2 dev: true + /reduce-flatten@2.0.0: + resolution: {integrity: sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==} + engines: {node: '>=6'} + dev: true + /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -6387,6 +6483,10 @@ packages: engines: {node: '>=10.0.0'} dev: true + /string-format@2.0.0: + resolution: {integrity: sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==} + dev: true + /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -6592,6 +6692,16 @@ packages: periscopic: 3.1.0 dev: true + /table-layout@1.0.2: + resolution: {integrity: sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==} + engines: {node: '>=8.0.0'} + dependencies: + array-back: 4.0.2 + deep-extend: 0.6.0 + typical: 5.2.0 + wordwrapjs: 4.0.1 + dev: true + /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true @@ -6670,6 +6780,24 @@ packages: utf8-byte-length: 1.0.4 dev: true + /ts-command-line-args@2.5.1: + resolution: {integrity: sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==} + hasBin: true + dependencies: + chalk: 4.1.2 + command-line-args: 5.2.1 + command-line-usage: 6.1.3 + string-format: 2.0.0 + dev: true + + /ts-essentials@7.0.3(typescript@5.1.6): + resolution: {integrity: sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==} + peerDependencies: + typescript: '>=3.7.0' + dependencies: + typescript: 5.1.6 + dev: true + /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} dev: true @@ -6738,12 +6866,43 @@ packages: engines: {node: '>=8'} dev: true + /typechain@8.3.1(typescript@5.1.6): + resolution: {integrity: sha512-fA7clol2IP/56yq6vkMTR+4URF1nGjV82Wx6Rf09EsqD4tkzMAvEaqYxVFCavJm/1xaRga/oD55K+4FtuXwQOQ==} + hasBin: true + peerDependencies: + typescript: '>=4.3.0' + dependencies: + '@types/prettier': 2.7.3 + debug: 4.3.4(supports-color@8.1.1) + fs-extra: 7.0.1 + glob: 7.1.7 + js-sha3: 0.8.0 + lodash: 4.17.21 + mkdirp: 1.0.4 + prettier: 2.8.8 + ts-command-line-args: 2.5.1 + ts-essentials: 7.0.3(typescript@5.1.6) + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + dev: true + /typescript@5.1.6: resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} engines: {node: '>=14.17'} hasBin: true dev: true + /typical@4.0.0: + resolution: {integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==} + engines: {node: '>=8'} + dev: true + + /typical@5.2.0: + resolution: {integrity: sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==} + engines: {node: '>=8'} + dev: true + /ufo@1.2.0: resolution: {integrity: sha512-RsPyTbqORDNDxqAdQPQBpgqhWle1VcTSou/FraClYlHf6TZnQcGslpLcAphNR+sQW4q5lLWLbOsRlh9j24baQg==} dev: true @@ -7023,6 +7182,14 @@ packages: stackback: 0.0.2 dev: true + /wordwrapjs@4.0.1: + resolution: {integrity: sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==} + engines: {node: '>=8.0.0'} + dependencies: + reduce-flatten: 2.0.0 + typical: 5.2.0 + dev: true + /workerpool@6.2.1: resolution: {integrity: sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==} dev: true diff --git a/src/lib/adapters/transaction.ts b/src/lib/adapters/transaction.ts index feec4729..6d3149ba 100644 --- a/src/lib/adapters/transaction.ts +++ b/src/lib/adapters/transaction.ts @@ -121,6 +121,36 @@ export async function sendTransaction( return tx } +export async function estimateTransaction( + wallet: BaseWallet, + to: string, + amount: bigint, + tokenAddress?: string, +): Promise { + const provider = getProvider() + const txWallet = wallet.connect(provider) + + let gasEstimate: bigint + + if (tokenAddress) { + const contract = new Contract(tokenAddress, abi, txWallet) + gasEstimate = await contract.transfer.estimateGas(to, amount) + } else { + const txRequest: TransactionRequest = { + to, + value: amount, + } + gasEstimate = await provider.estimateGas(txRequest) + } + const fee = await provider.getFeeData() + + if (fee.maxFeePerGas && fee.maxPriorityFeePerGas) + return gasEstimate * (fee.maxFeePerGas + fee.maxPriorityFeePerGas) + else if (fee.gasPrice) return gasEstimate * fee.gasPrice + + throw new Error('Could not estimate transaction fee') +} + export async function waitForTransaction( txHash: string, confirm?: number | undefined, diff --git a/src/lib/adapters/waku/index.ts b/src/lib/adapters/waku/index.ts index 71ff9295..fea17107 100644 --- a/src/lib/adapters/waku/index.ts +++ b/src/lib/adapters/waku/index.ts @@ -18,7 +18,11 @@ import { get } from 'svelte/store' import { objectStore, objectKey } from '$lib/stores/objects' import { lookup } from '$lib/objects/lookup' import type { TokenAmount } from '$lib/stores/balances' -import { defaultBlockchainNetwork, sendTransaction } from '$lib/adapters/transaction' +import { + defaultBlockchainNetwork, + sendTransaction, + estimateTransaction, +} from '$lib/adapters/transaction' import type { JSONSerializable, JSONValue, @@ -485,10 +489,11 @@ export default class WakuAdapter implements Adapter { return tx.hash } - async estimateTransaction(): Promise { + async estimateTransaction(wallet: Wallet, to: string, token: TokenAmount): Promise { + const fee = await estimateTransaction(wallet, to, token.amount, token.address) return { ...defaultBlockchainNetwork.nativeToken, - amount: 1000059237n, + amount: fee, } } diff --git a/src/lib/objects/split/blockchain.ts b/src/lib/objects/split/blockchain.ts index 6c2b4cba..31052c72 100644 --- a/src/lib/objects/split/blockchain.ts +++ b/src/lib/objects/split/blockchain.ts @@ -1,17 +1,35 @@ -import { Interface } from 'ethers' -import type { SplitterFactoryContract, SplitterContract, GetContract } from './types' -import splitterFactoryAbi from './abis/splitter-factory.json' -import splitterAbi from './abis/splitter.json' +import { Interface, type Provider } from 'ethers' +import type { GetContract } from './types' +import splitterFactoryAbi from './contracts/abis/splitter-factory.json' +import splitterAbi from './contracts/abis/splitter.json' import { defaultBlockchainNetwork } from '$lib/adapters/transaction' import type { Balance } from './schemas' +import type { Splitter, SplitterFactory } from './contracts/types' const splitterFactoryAddress = defaultBlockchainNetwork.objects.splitterFactory -function getSplitterContract(getContract: GetContract, splitterAddress: string): SplitterContract { - return getContract(splitterAddress, new Interface(splitterAbi)) as SplitterContract +function getSplitterContract(getContract: GetContract, splitterAddress: string): Splitter { + return getContract(splitterAddress, new Interface(splitterAbi)) as unknown as Splitter } -export function sleep(ms: number) { +function getSplitterContractFactory(getContract: GetContract): SplitterFactory { + return getContract( + splitterFactoryAddress, + new Interface(splitterFactoryAbi), + ) as unknown as SplitterFactory +} + +async function calculateFee(provider: Provider, gasEstimate: bigint): Promise { + const fee = await provider.getFeeData() + + if (fee.maxFeePerGas && fee.maxPriorityFeePerGas) + return gasEstimate * (fee.maxFeePerGas + fee.maxPriorityFeePerGas) + else if (fee.gasPrice) return gasEstimate * fee.gasPrice + + throw new Error('Could not estimate transaction fee') +} + +function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) } @@ -21,16 +39,17 @@ export async function createSplitterContract( token = '0x0000000000000000000000000000000000000000', metadata = '0x0000000000000000000000000000000000000000', ): Promise { - const splitterFactory = getContract( - splitterFactoryAddress, - new Interface(splitterFactoryAbi), - ) as SplitterFactoryContract + const splitterFactory = getSplitterContractFactory(getContract) + // TODO: this should be `once` with appropriate filter instead of `on` const events: { address: string; txHash: string }[] = [] - splitterFactory.addListener('SplitterCreated', (address, metadata, token, members, payload) => { - events.push({ address, txHash: payload.log.transactionHash }) - }) + splitterFactory.addListener( + splitterFactory.getEvent('SplitterCreated'), + (address, metadata, token, members, payload) => { + events.push({ address, txHash: payload.log.transactionHash }) + }, + ) // TODO: Maybe we don't need metadata const tx = await splitterFactory.create(metadata, token, members) @@ -52,13 +71,34 @@ export async function createSplitterContract( splitterAddress = events.find((e) => e.txHash === tx.hash)?.address await sleep(sleepTime) } - splitterFactory.removeAllListeners('SplitterCreated') + splitterFactory.removeAllListeners(splitterFactory.getEvent('SplitterCreated')) if (!splitterAddress) throw new Error('Failed to create splitter contract') return splitterAddress } +export async function estimateCreateSplitterContract( + getContract: GetContract, + members: string[], + token = '0x0000000000000000000000000000000000000000', + metadata = '0x0000000000000000000000000000000000000000', +): Promise { + const splitterFactory = getSplitterContractFactory(getContract) + + const gasEstimate: bigint = await splitterFactory.create.estimateGas(metadata, token, members) + + const provider = splitterFactory.runner?.provider + if (!provider) throw new Error('Could not estimate transaction fee') + + return calculateFee(provider, gasEstimate) +} + +export async function getMasterSplitterContractAddress(getContract: GetContract): Promise { + const splitterFactory = getSplitterContractFactory(getContract) + return await splitterFactory.masterSplitter() +} + export async function addExpense( getContract: GetContract, splitterAddress: string, @@ -77,6 +117,32 @@ export async function addExpense( return tx.hash } +export async function estimateAddExpense( + getContract: GetContract, + splitterAddress: string | undefined, + amount: bigint, + from: string, + members: string[], + metadata = '0x0000000000000000000000000000000000000000', +): Promise { + let gasEstimate: bigint + let splitter: Splitter + if (splitterAddress) { + splitter = getSplitterContract(getContract, splitterAddress) + gasEstimate = await splitter.addExpense.estimateGas(metadata, amount, from, members) + } else { + // This is worse case estimateAddExpense cost + // we can not do it by estimating the transaction because we don't have the splitter contract deployed and the transaction would fail in the master splitter contract + gasEstimate = 47530n + 9000n * BigInt(members.length - 2) + splitter = getSplitterContract(getContract, await getMasterSplitterContractAddress(getContract)) + } + + const provider = splitter.runner?.provider + if (!provider) throw new Error('Could not estimate transaction fee') + + return calculateFee(provider, gasEstimate) +} + export async function settleDebt( getContract: GetContract, splitterAddress: string, @@ -84,7 +150,7 @@ export async function settleDebt( ): Promise { const splitter = getSplitterContract(getContract, splitterAddress) const value = await splitter.debts(from) - const tx = await splitter.settleDebts(from, { gasLimit: 3000000n, value }) + const tx = await splitter['settleDebts(address)'](from, { gasLimit: 3000000n, value }) const receipt = await tx.wait() if (!receipt || receipt.status !== 1) { @@ -94,12 +160,28 @@ export async function settleDebt( return tx.hash } +export async function estimateSettleDebt( + getContract: GetContract, + splitterAddress: string, + from: string, +): Promise { + const splitter = getSplitterContract(getContract, splitterAddress) + const value = await splitter.debts(from) + const gasEstimate = await splitter['settleDebts(address)'].estimateGas(from, { value }) + + const provider = splitter.runner?.provider + if (!provider) throw new Error('Could not estimate transaction fee') + + return calculateFee(provider, gasEstimate) + + return 0n +} + export async function getBalances( getContract: GetContract, splitterAddress: string, ): Promise { - const splitter = getContract(splitterAddress, new Interface(splitterAbi)) as SplitterContract - + const splitter = getSplitterContract(getContract, splitterAddress) const members = await splitter.getMembers() const balances: Balance[] = [] @@ -113,3 +195,12 @@ export async function getBalances( return balances } + +export async function getOwedAmount( + getContract: GetContract, + splitterAddress: string, + from: string, +): Promise { + const splitter = getSplitterContract(getContract, splitterAddress) + return await splitter.debts(from) +} diff --git a/src/lib/objects/split/components/info.svelte b/src/lib/objects/split/components/info.svelte new file mode 100644 index 00000000..63cc22ae --- /dev/null +++ b/src/lib/objects/split/components/info.svelte @@ -0,0 +1,42 @@ + + +
+ {title} +
+ +
+ +
+
+
+ + diff --git a/src/lib/objects/split/abis/splitter-factory.json b/src/lib/objects/split/contracts/abis/splitter-factory.json similarity index 100% rename from src/lib/objects/split/abis/splitter-factory.json rename to src/lib/objects/split/contracts/abis/splitter-factory.json diff --git a/src/lib/objects/split/abis/splitter.json b/src/lib/objects/split/contracts/abis/splitter.json similarity index 100% rename from src/lib/objects/split/abis/splitter.json rename to src/lib/objects/split/contracts/abis/splitter.json diff --git a/src/lib/objects/split/contracts/types/Splitter.ts b/src/lib/objects/split/contracts/types/Splitter.ts new file mode 100644 index 00000000..b754a649 --- /dev/null +++ b/src/lib/objects/split/contracts/types/Splitter.ts @@ -0,0 +1,220 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +import type { + BaseContract, + BigNumberish, + BytesLike, + FunctionFragment, + Result, + Interface, + EventFragment, + AddressLike, + ContractRunner, + ContractMethod, + Listener, +} from 'ethers' +import type { + TypedContractEvent, + TypedDeferredTopicFilter, + TypedEventLog, + TypedLogDescription, + TypedListener, + TypedContractMethod, +} from './common' + +export interface SplitterInterface extends Interface { + getFunction( + nameOrSignature: + | 'addExpense' + | 'addMember' + | 'debts' + | 'getMembers' + | 'init' + | 'isMember' + | 'members' + | 'metadata' + | 'settleDebts(address)' + | 'settleDebts()' + | 'token', + ): FunctionFragment + + getEvent(nameOrSignatureOrTopic: 'ExpenseAdded'): EventFragment + + encodeFunctionData( + functionFragment: 'addExpense', + values: [BytesLike, BigNumberish, AddressLike, AddressLike[]], + ): string + encodeFunctionData(functionFragment: 'addMember', values: [AddressLike]): string + encodeFunctionData(functionFragment: 'debts', values: [AddressLike]): string + encodeFunctionData(functionFragment: 'getMembers', values?: undefined): string + encodeFunctionData( + functionFragment: 'init', + values: [BytesLike, AddressLike, AddressLike[]], + ): string + encodeFunctionData(functionFragment: 'isMember', values: [AddressLike]): string + encodeFunctionData(functionFragment: 'members', values: [BigNumberish]): string + encodeFunctionData(functionFragment: 'metadata', values?: undefined): string + encodeFunctionData(functionFragment: 'settleDebts(address)', values: [AddressLike]): string + encodeFunctionData(functionFragment: 'settleDebts()', values?: undefined): string + encodeFunctionData(functionFragment: 'token', values?: undefined): string + + decodeFunctionResult(functionFragment: 'addExpense', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'addMember', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'debts', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'getMembers', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'init', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'isMember', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'members', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'metadata', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'settleDebts(address)', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'settleDebts()', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'token', data: BytesLike): Result +} + +export namespace ExpenseAddedEvent { + export type InputTuple = [ + metadata: BytesLike, + amount: BigNumberish, + paidBy: AddressLike, + targets: AddressLike[], + ] + export type OutputTuple = [metadata: string, amount: bigint, paidBy: string, targets: string[]] + export interface OutputObject { + metadata: string + amount: bigint + paidBy: string + targets: string[] + } + export type Event = TypedContractEvent + export type Filter = TypedDeferredTopicFilter + export type Log = TypedEventLog + export type LogDescription = TypedLogDescription +} + +export interface Splitter extends BaseContract { + connect(runner?: ContractRunner | null): Splitter + waitForDeployment(): Promise + + interface: SplitterInterface + + queryFilter( + event: TCEvent, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined, + ): Promise>> + queryFilter( + filter: TypedDeferredTopicFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined, + ): Promise>> + + on( + event: TCEvent, + listener: TypedListener, + ): Promise + on( + filter: TypedDeferredTopicFilter, + listener: TypedListener, + ): Promise + + once( + event: TCEvent, + listener: TypedListener, + ): Promise + once( + filter: TypedDeferredTopicFilter, + listener: TypedListener, + ): Promise + + listeners( + event: TCEvent, + ): Promise>> + listeners(eventName?: string): Promise> + removeAllListeners(event?: TCEvent): Promise + + addExpense: TypedContractMethod< + [_metadata: BytesLike, _amount: BigNumberish, _payor: AddressLike, _targets: AddressLike[]], + [void], + 'nonpayable' + > + + addMember: TypedContractMethod<[member: AddressLike], [void], 'nonpayable'> + + debts: TypedContractMethod<[arg0: AddressLike], [bigint], 'view'> + + getMembers: TypedContractMethod<[], [string[]], 'view'> + + init: TypedContractMethod< + [_metadata: BytesLike, _token: AddressLike, _members: AddressLike[]], + [void], + 'nonpayable' + > + + isMember: TypedContractMethod<[arg0: AddressLike], [boolean], 'view'> + + members: TypedContractMethod<[arg0: BigNumberish], [string], 'view'> + + metadata: TypedContractMethod<[], [string], 'view'> + + 'settleDebts(address)': TypedContractMethod<[user: AddressLike], [void], 'payable'> + + 'settleDebts()': TypedContractMethod<[], [void], 'payable'> + + token: TypedContractMethod<[], [string], 'view'> + + getFunction(key: string | FunctionFragment): T + + getFunction( + nameOrSignature: 'addExpense', + ): TypedContractMethod< + [_metadata: BytesLike, _amount: BigNumberish, _payor: AddressLike, _targets: AddressLike[]], + [void], + 'nonpayable' + > + getFunction( + nameOrSignature: 'addMember', + ): TypedContractMethod<[member: AddressLike], [void], 'nonpayable'> + getFunction(nameOrSignature: 'debts'): TypedContractMethod<[arg0: AddressLike], [bigint], 'view'> + getFunction(nameOrSignature: 'getMembers'): TypedContractMethod<[], [string[]], 'view'> + getFunction( + nameOrSignature: 'init', + ): TypedContractMethod< + [_metadata: BytesLike, _token: AddressLike, _members: AddressLike[]], + [void], + 'nonpayable' + > + getFunction( + nameOrSignature: 'isMember', + ): TypedContractMethod<[arg0: AddressLike], [boolean], 'view'> + getFunction( + nameOrSignature: 'members', + ): TypedContractMethod<[arg0: BigNumberish], [string], 'view'> + getFunction(nameOrSignature: 'metadata'): TypedContractMethod<[], [string], 'view'> + getFunction( + nameOrSignature: 'settleDebts(address)', + ): TypedContractMethod<[user: AddressLike], [void], 'payable'> + getFunction(nameOrSignature: 'settleDebts()'): TypedContractMethod<[], [void], 'payable'> + getFunction(nameOrSignature: 'token'): TypedContractMethod<[], [string], 'view'> + + getEvent( + key: 'ExpenseAdded', + ): TypedContractEvent< + ExpenseAddedEvent.InputTuple, + ExpenseAddedEvent.OutputTuple, + ExpenseAddedEvent.OutputObject + > + + filters: { + 'ExpenseAdded(bytes,uint256,address,address[])': TypedContractEvent< + ExpenseAddedEvent.InputTuple, + ExpenseAddedEvent.OutputTuple, + ExpenseAddedEvent.OutputObject + > + ExpenseAdded: TypedContractEvent< + ExpenseAddedEvent.InputTuple, + ExpenseAddedEvent.OutputTuple, + ExpenseAddedEvent.OutputObject + > + } +} diff --git a/src/lib/objects/split/contracts/types/SplitterFactory.ts b/src/lib/objects/split/contracts/types/SplitterFactory.ts new file mode 100644 index 00000000..40c90d46 --- /dev/null +++ b/src/lib/objects/split/contracts/types/SplitterFactory.ts @@ -0,0 +1,140 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +import type { + BaseContract, + BytesLike, + FunctionFragment, + Result, + Interface, + EventFragment, + AddressLike, + ContractRunner, + ContractMethod, + Listener, +} from 'ethers' +import type { + TypedContractEvent, + TypedDeferredTopicFilter, + TypedEventLog, + TypedLogDescription, + TypedListener, + TypedContractMethod, +} from './common' + +export interface SplitterFactoryInterface extends Interface { + getFunction(nameOrSignature: 'create' | 'masterSplitter'): FunctionFragment + + getEvent(nameOrSignatureOrTopic: 'SplitterCreated'): EventFragment + + encodeFunctionData( + functionFragment: 'create', + values: [BytesLike, AddressLike, AddressLike[]], + ): string + encodeFunctionData(functionFragment: 'masterSplitter', values?: undefined): string + + decodeFunctionResult(functionFragment: 'create', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'masterSplitter', data: BytesLike): Result +} + +export namespace SplitterCreatedEvent { + export type InputTuple = [ + addr: AddressLike, + _metadata: BytesLike, + _token: AddressLike, + _members: AddressLike[], + ] + export type OutputTuple = [addr: string, _metadata: string, _token: string, _members: string[]] + export interface OutputObject { + addr: string + _metadata: string + _token: string + _members: string[] + } + export type Event = TypedContractEvent + export type Filter = TypedDeferredTopicFilter + export type Log = TypedEventLog + export type LogDescription = TypedLogDescription +} + +export interface SplitterFactory extends BaseContract { + connect(runner?: ContractRunner | null): SplitterFactory + waitForDeployment(): Promise + + interface: SplitterFactoryInterface + + queryFilter( + event: TCEvent, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined, + ): Promise>> + queryFilter( + filter: TypedDeferredTopicFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined, + ): Promise>> + + on( + event: TCEvent, + listener: TypedListener, + ): Promise + on( + filter: TypedDeferredTopicFilter, + listener: TypedListener, + ): Promise + + once( + event: TCEvent, + listener: TypedListener, + ): Promise + once( + filter: TypedDeferredTopicFilter, + listener: TypedListener, + ): Promise + + listeners( + event: TCEvent, + ): Promise>> + listeners(eventName?: string): Promise> + removeAllListeners(event?: TCEvent): Promise + + create: TypedContractMethod< + [_metadata: BytesLike, _token: AddressLike, _members: AddressLike[]], + [string], + 'nonpayable' + > + + masterSplitter: TypedContractMethod<[], [string], 'view'> + + getFunction(key: string | FunctionFragment): T + + getFunction( + nameOrSignature: 'create', + ): TypedContractMethod< + [_metadata: BytesLike, _token: AddressLike, _members: AddressLike[]], + [string], + 'nonpayable' + > + getFunction(nameOrSignature: 'masterSplitter'): TypedContractMethod<[], [string], 'view'> + + getEvent( + key: 'SplitterCreated', + ): TypedContractEvent< + SplitterCreatedEvent.InputTuple, + SplitterCreatedEvent.OutputTuple, + SplitterCreatedEvent.OutputObject + > + + filters: { + 'SplitterCreated(address,bytes,address,address[])': TypedContractEvent< + SplitterCreatedEvent.InputTuple, + SplitterCreatedEvent.OutputTuple, + SplitterCreatedEvent.OutputObject + > + SplitterCreated: TypedContractEvent< + SplitterCreatedEvent.InputTuple, + SplitterCreatedEvent.OutputTuple, + SplitterCreatedEvent.OutputObject + > + } +} diff --git a/src/lib/objects/split/contracts/types/common.ts b/src/lib/objects/split/contracts/types/common.ts new file mode 100644 index 00000000..2eecc7c1 --- /dev/null +++ b/src/lib/objects/split/contracts/types/common.ts @@ -0,0 +1,102 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +import type { + FunctionFragment, + Typed, + EventFragment, + ContractTransaction, + ContractTransactionResponse, + DeferredTopicFilter, + EventLog, + TransactionRequest, + LogDescription, +} from 'ethers' + +export interface TypedDeferredTopicFilter<_TCEvent extends TypedContractEvent> + extends DeferredTopicFilter {} + +export interface TypedContractEvent< + InputTuple extends Array = any, + OutputTuple extends Array = any, + OutputObject = any, +> { + (...args: Partial): TypedDeferredTopicFilter< + TypedContractEvent + > + name: string + fragment: EventFragment + getFragment(...args: Partial): EventFragment +} + +type __TypechainAOutputTuple = T extends TypedContractEvent ? W : never +type __TypechainOutputObject = T extends TypedContractEvent + ? V + : never + +export interface TypedEventLog extends Omit { + args: __TypechainAOutputTuple & __TypechainOutputObject +} + +export interface TypedLogDescription + extends Omit { + args: __TypechainAOutputTuple & __TypechainOutputObject +} + +export type TypedListener = ( + ...listenerArg: [...__TypechainAOutputTuple, TypedEventLog, ...undefined[]] +) => void + +export type MinEthersFactory = { + deploy(...a: ARGS[]): Promise +} + +export type GetContractTypeFromFactory = F extends MinEthersFactory ? C : never +export type GetARGsTypeFromFactory = F extends MinEthersFactory + ? Parameters + : never + +export type StateMutability = 'nonpayable' | 'payable' | 'view' + +export type BaseOverrides = Omit +export type NonPayableOverrides = Omit +export type PayableOverrides = Omit +export type ViewOverrides = Omit +export type Overrides = S extends 'nonpayable' + ? NonPayableOverrides + : S extends 'payable' + ? PayableOverrides + : ViewOverrides + +export type PostfixOverrides, S extends StateMutability> = + | A + | [...A, Overrides] +export type ContractMethodArgs, S extends StateMutability> = PostfixOverrides< + { [I in keyof A]-?: A[I] | Typed }, + S +> + +export type DefaultReturnType = R extends Array ? R[0] : R + +// export interface ContractMethod = Array, R = any, D extends R | ContractTransactionResponse = R | ContractTransactionResponse> { +export interface TypedContractMethod< + A extends Array = Array, + R = any, + S extends StateMutability = 'payable', +> { + (...args: ContractMethodArgs): S extends 'view' + ? Promise> + : Promise + + name: string + + fragment: FunctionFragment + + getFragment(...args: ContractMethodArgs): FunctionFragment + + populateTransaction(...args: ContractMethodArgs): Promise + staticCall(...args: ContractMethodArgs): Promise> + send(...args: ContractMethodArgs): Promise + estimateGas(...args: ContractMethodArgs): Promise + staticCallResult(...args: ContractMethodArgs): Promise +} diff --git a/src/lib/objects/split/contracts/types/factories/SplitterFactory__factory.ts b/src/lib/objects/split/contracts/types/factories/SplitterFactory__factory.ts new file mode 100644 index 00000000..ca49b3fc --- /dev/null +++ b/src/lib/objects/split/contracts/types/factories/SplitterFactory__factory.ts @@ -0,0 +1,97 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ + +import { Contract, Interface, type ContractRunner } from 'ethers' +import type { SplitterFactory, SplitterFactoryInterface } from '../SplitterFactory' + +const _abi = [ + { + inputs: [], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'addr', + type: 'address', + }, + { + indexed: false, + internalType: 'bytes', + name: '_metadata', + type: 'bytes', + }, + { + indexed: false, + internalType: 'contract ERC20', + name: '_token', + type: 'address', + }, + { + indexed: false, + internalType: 'address[]', + name: '_members', + type: 'address[]', + }, + ], + name: 'SplitterCreated', + type: 'event', + }, + { + inputs: [ + { + internalType: 'bytes', + name: '_metadata', + type: 'bytes', + }, + { + internalType: 'contract ERC20', + name: '_token', + type: 'address', + }, + { + internalType: 'address[]', + name: '_members', + type: 'address[]', + }, + ], + name: 'create', + outputs: [ + { + internalType: 'contract Splitter', + name: 'splitter', + type: 'address', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'masterSplitter', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, +] as const + +export class SplitterFactory__factory { + static readonly abi = _abi + static createInterface(): SplitterFactoryInterface { + return new Interface(_abi) as SplitterFactoryInterface + } + static connect(address: string, runner?: ContractRunner | null): SplitterFactory { + return new Contract(address, _abi, runner) as unknown as SplitterFactory + } +} diff --git a/src/lib/objects/split/contracts/types/factories/Splitter__factory.ts b/src/lib/objects/split/contracts/types/factories/Splitter__factory.ts new file mode 100644 index 00000000..4b318812 --- /dev/null +++ b/src/lib/objects/split/contracts/types/factories/Splitter__factory.ts @@ -0,0 +1,282 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ + +import { Contract, Interface, type ContractRunner } from 'ethers' +import type { Splitter, SplitterInterface } from '../Splitter' + +const _abi = [ + { + inputs: [], + name: 'AlreadyAMember', + type: 'error', + }, + { + inputs: [], + name: 'AlreadyInitialized', + type: 'error', + }, + { + inputs: [], + name: 'IncorrectValue', + type: 'error', + }, + { + inputs: [ + { + internalType: 'address', + name: 'user', + type: 'address', + }, + { + internalType: 'int256', + name: 'debt', + type: 'int256', + }, + ], + name: 'NoDebt', + type: 'error', + }, + { + inputs: [], + name: 'NoTargets', + type: 'error', + }, + { + inputs: [ + { + internalType: 'address', + name: 'target', + type: 'address', + }, + ], + name: 'NotAMember', + type: 'error', + }, + { + inputs: [], + name: 'ValueNotZero', + type: 'error', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'bytes', + name: 'metadata', + type: 'bytes', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + indexed: false, + internalType: 'address', + name: 'paidBy', + type: 'address', + }, + { + indexed: false, + internalType: 'address[]', + name: 'targets', + type: 'address[]', + }, + ], + name: 'ExpenseAdded', + type: 'event', + }, + { + inputs: [ + { + internalType: 'bytes', + name: '_metadata', + type: 'bytes', + }, + { + internalType: 'uint256', + name: '_amount', + type: 'uint256', + }, + { + internalType: 'address', + name: '_payor', + type: 'address', + }, + { + internalType: 'address[]', + name: '_targets', + type: 'address[]', + }, + ], + name: 'addExpense', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'member', + type: 'address', + }, + ], + name: 'addMember', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + name: 'debts', + outputs: [ + { + internalType: 'int256', + name: '', + type: 'int256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getMembers', + outputs: [ + { + internalType: 'address[]', + name: '', + type: 'address[]', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'bytes', + name: '_metadata', + type: 'bytes', + }, + { + internalType: 'contract ERC20', + name: '_token', + type: 'address', + }, + { + internalType: 'address[]', + name: '_members', + type: 'address[]', + }, + ], + name: 'init', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + name: 'isMember', + outputs: [ + { + internalType: 'bool', + name: '', + type: 'bool', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + name: 'members', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'metadata', + outputs: [ + { + internalType: 'bytes', + name: '', + type: 'bytes', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'user', + type: 'address', + }, + ], + name: 'settleDebts', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [], + name: 'settleDebts', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [], + name: 'token', + outputs: [ + { + internalType: 'contract ERC20', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, +] as const + +export class Splitter__factory { + static readonly abi = _abi + static createInterface(): SplitterInterface { + return new Interface(_abi) as SplitterInterface + } + static connect(address: string, runner?: ContractRunner | null): Splitter { + return new Contract(address, _abi, runner) as unknown as Splitter + } +} diff --git a/src/lib/objects/split/contracts/types/factories/index.ts b/src/lib/objects/split/contracts/types/factories/index.ts new file mode 100644 index 00000000..45473f12 --- /dev/null +++ b/src/lib/objects/split/contracts/types/factories/index.ts @@ -0,0 +1,5 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +export { Splitter__factory } from './Splitter__factory' +export { SplitterFactory__factory } from './SplitterFactory__factory' diff --git a/src/lib/objects/split/contracts/types/index.ts b/src/lib/objects/split/contracts/types/index.ts new file mode 100644 index 00000000..1e067c7b --- /dev/null +++ b/src/lib/objects/split/contracts/types/index.ts @@ -0,0 +1,8 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +export type { Splitter } from './Splitter' +export type { SplitterFactory } from './SplitterFactory' +export * as factories from './factories' +export { SplitterFactory__factory } from './factories/SplitterFactory__factory' +export { Splitter__factory } from './factories/Splitter__factory' diff --git a/src/lib/objects/split/standalone.svelte b/src/lib/objects/split/standalone.svelte index da33f39f..95d4242e 100644 --- a/src/lib/objects/split/standalone.svelte +++ b/src/lib/objects/split/standalone.svelte @@ -1,4 +1,5 @@ @@ -74,9 +87,11 @@ - {#if nativeToken === undefined || splitToken === undefined || fee === undefined} + {#if nativeToken === undefined || splitToken === undefined}

No native token

+ {:else if owedAmount === undefined} +

Loading...

{:else}

{chatName} shared expenses

@@ -88,16 +103,10 @@ {#if owedAmount > 0n} -
- Amount to settle -
- -
- {toSignificant(owedAmount, splitToken.decimals)} - {nativeToken.symbol} -
-
-
+ + {toSignificant(owedAmount, splitToken.decimals)} + {nativeToken.symbol} +

{toSignificant(nativeToken.amount, nativeToken.decimals)} @@ -107,43 +116,44 @@ > -

- From -
- -
-

Your account

-

- {formatAddress(profile.address, 6, 6)} -

-
-
-
+ +

Your account

+

+ {formatAddress(profile.address, 6, 6)} +

+
-
- Transaction fee (max) -
- -
-

{toSignificant(fee.amount, fee.decimals)} {fee.symbol}

-

- {toSignificant(fee.amount, fee.decimals)} ≈ {toSignificant( - fee.amount, - fee.decimals, - )} - DAI -

-
-
-
- -

- {toSignificant(splitToken.amount, splitToken.decimals)} - {splitToken.symbol} available -

-
+ {#if fee} + +

{toSignificant(fee, nativeToken.decimals)} {nativeToken.symbol}

+

+ {toSignificant(fee, nativeToken.decimals)} ≈ TODO DAI +

+
+ +

+ {toSignificant(splitToken.amount, splitToken.decimals)} + {splitToken.symbol} available +

+
+ {:else if !hasEnoughFunds} +

You don't have enough funds to settle

+ {:else if feeChecking} + +

Loading...

+
+ +

+ {toSignificant(splitToken.amount, splitToken.decimals)} + {splitToken.symbol} available +

+
+ {:else} +

Failed to check fee

+

{feeError?.message}

+ {/if}
{:else if owedAmount < 0n} @@ -162,7 +172,7 @@ @@ -173,31 +183,4 @@ img { border-radius: var(--spacing-12); } - - .label { - display: flex; - flex-direction: column; - gap: var(--spacing-6); - } - - .label span { - margin-inline: 13px; - text-align: left; - color: var(--color-step-40, var(--color-dark-step-20)); - } - - .input-wrapper { - position: relative; - } - - .input { - border: 1px solid var(--color-step-20, var(--color-dark-step-40)); - border-radius: var(--border-radius); - padding: 11px var(--spacing-12); - max-height: 120px; - min-height: 48px; - width: 100%; - color: var(--color-step-40, var(--color-dark-step-20)); - background-color: var(--color-base, var(--color-dark-accent)); - } diff --git a/src/lib/objects/split/views/summary.svelte b/src/lib/objects/split/views/summary.svelte index 0109dda4..821d7d65 100644 --- a/src/lib/objects/split/views/summary.svelte +++ b/src/lib/objects/split/views/summary.svelte @@ -14,10 +14,16 @@ import type { DataMessage } from '../schemas' import { splitDescriptor } from '..' import type { User } from '$lib/types' - import { toBigInt } from '$lib/utils/format' - import { addExpense, createSplitterContract } from '../blockchain' + import { toBigInt, toSignificant } from '$lib/utils/format' + import { + addExpense, + createSplitterContract, + estimateAddExpense, + estimateCreateSplitterContract, + } from '../blockchain' import type { GetContract } from '../types' - import type { Token } from '$lib/objects/schemas' + import type { Token, TokenAmount } from '$lib/objects/schemas' + import Info from '../components/info.svelte' export let amount: string export let description: string @@ -28,11 +34,38 @@ export let splitterAddress: string | undefined export let instanceId: string export let token: Token + export let nativeToken: TokenAmount export let send: (message: DataMessage) => Promise export let exitObject: () => void export let getContract: GetContract let transactionSent = false + let fee: bigint | undefined = undefined + + async function estimateFee(users: User[], amount: string, token: Token) { + const members = users.map((u) => u.address) + let amnt = toBigInt(amount, token.decimals) + let fee = 0n + let splitContractAddress = splitterAddress + + if (!splitContractAddress) { + fee = await estimateCreateSplitterContract(getContract, members) + } + fee += await estimateAddExpense( + getContract, + splitContractAddress, + amnt, + profile.address, + members, + ) + + return fee + } + + $: if (users && amount && token) + estimateFee(users, amount, token) + .then((amount) => (fee = amount)) + .catch(console.error) async function sendTransactionInternal() { transactionSent = true @@ -87,19 +120,35 @@ - - + +
{chatName}
-
- + +
#{instanceId.slice(0, 4)}
-
- + +
{amount} {token.symbol}
-
- + +
{description}
-
+ + + + +
+ {fee ? toSignificant(fee, nativeToken.decimals) : 'unknown'} + {nativeToken.symbol} +
+
+ +

+ {toSignificant(nativeToken.amount, nativeToken.decimals)} + {nativeToken.symbol} available +

+
+
+ {#if images.length > 0} {#each images as image}