From c0112691aadc06ef7ba6fbad07ca5cdb7ad248c3 Mon Sep 17 00:00:00 2001 From: pragmaxim Date: Fri, 20 Sep 2024 09:24:25 +0200 Subject: [PATCH] mutli-group-transfer --- packages/walletconnect/src/constants.ts | 1 + packages/walletconnect/src/provider.ts | 4 + packages/walletconnect/src/types.ts | 4 + .../walletconnect/test/shared/WalletClient.ts | 3 + packages/web3/src/api/api-alephium.ts | 50 +++++--- packages/web3/src/signer/signer.ts | 25 ++++ packages/web3/src/signer/tx-builder.ts | 22 ++++ test/tipping-multi-group.test.ts | 118 ++++++++++++++++++ 8 files changed, 213 insertions(+), 14 deletions(-) create mode 100644 test/tipping-multi-group.test.ts diff --git a/packages/walletconnect/src/constants.ts b/packages/walletconnect/src/constants.ts index 1d48d6039..17dbab33b 100644 --- a/packages/walletconnect/src/constants.ts +++ b/packages/walletconnect/src/constants.ts @@ -22,6 +22,7 @@ export const PROVIDER_NAMESPACE = 'alephium' // 2. `alph_signUnsignedTx` can be used for complicated transactions (e.g. multisig). export const RELAY_METHODS = [ 'alph_signAndSubmitTransferTx', + 'alph_signAndSubmitMultiGroupTransferTx', 'alph_signAndSubmitDeployContractTx', 'alph_signAndSubmitExecuteScriptTx', 'alph_signAndSubmitUnsignedTx', diff --git a/packages/walletconnect/src/provider.ts b/packages/walletconnect/src/provider.ts index 8027aa071..c3bcb35bb 100644 --- a/packages/walletconnect/src/provider.ts +++ b/packages/walletconnect/src/provider.ts @@ -210,6 +210,10 @@ export class WalletConnectProvider extends SignerProvider { return this.typedRequest('alph_signAndSubmitTransferTx', params) } + public async signAndSubmitMultiGroupTransferTx(params: SignTransferTxParams): Promise { + return this.typedRequest('alph_signAndSubmitMultiGroupTransferTx', params) + } + public async signAndSubmitDeployContractTx(params: SignDeployContractTxParams): Promise { return this.typedRequest('alph_signAndSubmitDeployContractTx', params) } diff --git a/packages/walletconnect/src/types.ts b/packages/walletconnect/src/types.ts index b72c076d4..ebe64508c 100644 --- a/packages/walletconnect/src/types.ts +++ b/packages/walletconnect/src/types.ts @@ -43,6 +43,10 @@ type RelayMethodsTable = { params: SignTransferTxParams result: SignTransferTxResult } + alph_signAndSubmitMultiGroupTransferTx: { + params: SignTransferTxParams + result: SignTransferTxResult[] + } alph_signAndSubmitDeployContractTx: { params: SignDeployContractTxParams result: SignDeployContractTxResult diff --git a/packages/walletconnect/test/shared/WalletClient.ts b/packages/walletconnect/test/shared/WalletClient.ts index 4be251193..dbb3bb821 100644 --- a/packages/walletconnect/test/shared/WalletClient.ts +++ b/packages/walletconnect/test/shared/WalletClient.ts @@ -277,6 +277,9 @@ export class WalletClient { case 'alph_signAndSubmitTransferTx': result = await this.signer.signAndSubmitTransferTx(request.params as any as SignTransferTxParams) break + case 'alph_signAndSubmitMultiGroupTransferTx': + result = await this.signer.signAndSubmitMultiGroupTransferTx(request.params as any as SignTransferTxParams) + break case 'alph_signAndSubmitDeployContractTx': result = await this.signer.signAndSubmitDeployContractTx( request.params as any as SignDeployContractTxParams diff --git a/packages/web3/src/api/api-alephium.ts b/packages/web3/src/api/api-alephium.ts index f1e9a4b2c..730d691c3 100644 --- a/packages/web3/src/api/api-alephium.ts +++ b/packages/web3/src/api/api-alephium.ts @@ -1407,8 +1407,8 @@ export class HttpClient { property instanceof Blob ? property : typeof property === 'object' && property !== null - ? JSON.stringify(property) - : `${property}` + ? JSON.stringify(property) + : `${property}` ) return formData }, new FormData()), @@ -1488,18 +1488,18 @@ export class HttpClient { const data = !responseFormat ? r : await response[responseFormat]() - .then((data) => { - if (r.ok) { - r.data = data - } else { - r.error = data - } - return r - }) - .catch((e) => { - r.error = e - return r - }) + .then((data) => { + if (r.ok) { + r.data = data + } else { + r.error = data + } + return r + }) + .catch((e) => { + r.error = e + return r + }) if (cancelToken) { this.abortControllers.delete(cancelToken) @@ -2402,6 +2402,28 @@ export class Api extends HttpClient + this.request< + BuildTransactionResult[], + BadRequest | Unauthorized | NotFound | InternalServerError | ServiceUnavailable + >({ + path: `/transactions/build-multi-group`, + method: 'POST', + body: data, + type: ContentType.Json, + format: 'json', + ...params + }).then(convertHttpResponse), + + /** * No description * diff --git a/packages/web3/src/signer/signer.ts b/packages/web3/src/signer/signer.ts index 511ed1a06..d5edf29d7 100644 --- a/packages/web3/src/signer/signer.ts +++ b/packages/web3/src/signer/signer.ts @@ -65,6 +65,7 @@ export abstract class SignerProvider { } abstract signAndSubmitTransferTx(params: SignTransferTxParams): Promise + abstract signAndSubmitMultiGroupTransferTx(params: SignTransferTxParams): Promise abstract signAndSubmitDeployContractTx(params: SignDeployContractTxParams): Promise abstract signAndSubmitExecuteScriptTx(params: SignExecuteScriptTxParams): Promise abstract signAndSubmitUnsignedTx(params: SignUnsignedTxParams): Promise @@ -102,6 +103,13 @@ export abstract class SignerProviderSimple extends SignerProvider { await this.submitTransaction(signResult) return signResult } + async signAndSubmitMultiGroupTransferTx(params: SignTransferTxParams): Promise { + const signResults = await this.signMultiGroupTransferTx(params) + for (const signedTx of signResults) { + await this.submitTransaction(signedTx) + } + return signResults + } async signAndSubmitDeployContractTx(params: SignDeployContractTxParams): Promise { const signResult = await this.signDeployContractTx(params) await this.submitTransaction(signResult) @@ -141,6 +149,23 @@ export abstract class SignerProviderSimple extends SignerProvider { ) } + async signMultiGroupTransferTx(params: SignTransferTxParams): Promise { + const results = await this.buildMultiGroupTransferTx(params) + const signedTxResults = await Promise.all(results.map(async (tx) => { + const signature = await this.signRaw(params.signerAddress, tx.txId) + return { ...tx, signature } + })) + + return signedTxResults + } + + async buildMultiGroupTransferTx(params: SignTransferTxParams): Promise[]> { + return await TransactionBuilder.from(this.nodeProvider).buildMultiGroupTransferTx( + params, + await this.getPublicKey(params.signerAddress) + ) + } + async signDeployContractTx(params: SignDeployContractTxParams): Promise { const response = await this.buildDeployContractTx(params) const signature = await this.signRaw(params.signerAddress, response.txId) diff --git a/packages/web3/src/signer/tx-builder.ts b/packages/web3/src/signer/tx-builder.ts index 7bc938457..2d1aaaf98 100644 --- a/packages/web3/src/signer/tx-builder.ts +++ b/packages/web3/src/signer/tx-builder.ts @@ -76,6 +76,28 @@ export abstract class TransactionBuilder { return { ...response, gasPrice: fromApiNumber256(response.gasPrice) } } + async buildMultiGroupTransferTx( + params: SignTransferTxParams, + publicKey: string + ): Promise[]> { + TransactionBuilder.validatePublicKey(params, publicKey, params.signerKeyType) + + const { destinations, gasPrice, ...rest } = params + const data: node.BuildTransaction = { + fromPublicKey: publicKey, + fromPublicKeyType: params.signerKeyType, + destinations: toApiDestinations(destinations), + gasPrice: toApiNumber256Optional(gasPrice), + ...rest + } + const results = await this.nodeProvider.transactions.postTransactionsBuildMultiGroup(data) + const response = results.map((result) => ({ + ...result, + gasPrice: fromApiNumber256(result.gasPrice) + })) + return response + } + async buildDeployContractTx( params: SignDeployContractTxParams, publicKey: string diff --git a/test/tipping-multi-group.test.ts b/test/tipping-multi-group.test.ts new file mode 100644 index 000000000..a2e57aac1 --- /dev/null +++ b/test/tipping-multi-group.test.ts @@ -0,0 +1,118 @@ +/* +Copyright 2018 - 2022 The Alephium Authors +This file is part of the alephium project. + +The library is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +The library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with the library. If not, see . +*/ + +import { NodeProvider, convertAlphAmountWithDecimals, number256ToNumber, DEFAULT_GAS_ALPH_AMOUNT } from '@alephium/web3' +import { testNodeWallet } from '@alephium/web3-test' +import { PrivateKeyWallet, deriveHDWalletPrivateKeyForGroup } from '@alephium/web3-wallet' +import * as bip39 from 'bip39' + +class MultiGroupTippingBot { + private readonly nodeProvider: NodeProvider // This can be initialized with node url + api key in a real application + private readonly mnemonic: string // This should be stored securely in a real application + readonly userGroups: Map + + constructor(nodeProvider: NodeProvider, mnemonic: string) { + this.nodeProvider = nodeProvider + this.mnemonic = mnemonic + this.userGroups = new Map() + } + + addUser(userId: string): PrivateKeyWallet { + if (this.userGroups.has(userId)) { + throw new Error(`User ${userId} already exists`) + } + + const groupNumber = this.userGroups.size + this.userGroups.set(userId, groupNumber) + return this.getUserWallet(userId) + } + + getUserWallet(userId: string): PrivateKeyWallet { + const groupNumber = this.userGroups.get(userId) + if (groupNumber === undefined) { + throw new Error(`User ${userId} does not exist`) + } + + const [privateKey, _addressIndex] = deriveHDWalletPrivateKeyForGroup(this.mnemonic, groupNumber, 'default') + return new PrivateKeyWallet({ privateKey, nodeProvider: this.nodeProvider }) + } + + getUserAddress(userId: string) { + return this.getUserWallet(userId).address + } + + async getUserBalance(userId: string) { + const userWallet = this.getUserWallet(userId) + const balance = await userWallet.nodeProvider.addresses.getAddressesAddressBalance(userWallet.address) + return number256ToNumber(balance.balance, 18) + } + + async sendTips(fromUserId: string, toUserData: [string, number][]) { + const fromUserWallet = this.getUserWallet(fromUserId) + + const destinations = toUserData.map(([user, amount]) => ({ + address: this.getUserAddress(user), + attoAlphAmount: convertAlphAmountWithDecimals(amount)! + })) + + await fromUserWallet.signAndSubmitMultiGroupTransferTx({ + signerAddress: fromUserWallet.address, + destinations: destinations + }) + } +} + +describe('tippingbot', function () { + it('should work', async function () { + const nodeProvider = new NodeProvider('http://127.0.0.1:22973') + const mnemonic = bip39.generateMnemonic() + const tippingBot = new MultiGroupTippingBot(nodeProvider, mnemonic) + + // deposit 1 ALPH for each user + const testWallet = await testNodeWallet() + const signerAddress = (await testWallet.getSelectedAccount()).address + + const users = ['user0', 'user1', 'user2'] + const destinations = users.map(user => ({ + address: tippingBot.addUser(user).address, + attoAlphAmount: convertAlphAmountWithDecimals('1.0')! + })) + + await testWallet.signAndSubmitMultiGroupTransferTx({ + signerAddress, + destinations + }) + + // check user balance + for (const user of users) { + const balance = await tippingBot.getUserBalance(user) + expect(balance).toEqual(1.0) + } + + await tippingBot.sendTips('user0', [['user1', 0.1], ['user2', 0.2]]) + await tippingBot.sendTips('user1', [['user2', 0.3]]) + + // check user balance + const balance0 = await tippingBot.getUserBalance('user0') + const balance1 = await tippingBot.getUserBalance('user1') + const balance2 = await tippingBot.getUserBalance('user2') + expect(balance0).toEqual(1.0 - 0.1 - 0.2 - DEFAULT_GAS_ALPH_AMOUNT * 2) + expect(balance1).toEqual(1.0 - 0.2 - DEFAULT_GAS_ALPH_AMOUNT) + expect(balance2).toEqual(1.0 + 0.2 + 0.3) + }) +})