Skip to content

Commit

Permalink
mutli-group-transfer
Browse files Browse the repository at this point in the history
  • Loading branch information
pragmaxim committed Sep 20, 2024
1 parent 99b8d4d commit c011269
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 14 deletions.
1 change: 1 addition & 0 deletions packages/walletconnect/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 4 additions & 0 deletions packages/walletconnect/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ export class WalletConnectProvider extends SignerProvider {
return this.typedRequest('alph_signAndSubmitTransferTx', params)
}

public async signAndSubmitMultiGroupTransferTx(params: SignTransferTxParams): Promise<SignTransferTxResult[]> {
return this.typedRequest('alph_signAndSubmitMultiGroupTransferTx', params)
}

public async signAndSubmitDeployContractTx(params: SignDeployContractTxParams): Promise<SignDeployContractTxResult> {
return this.typedRequest('alph_signAndSubmitDeployContractTx', params)
}
Expand Down
4 changes: 4 additions & 0 deletions packages/walletconnect/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ type RelayMethodsTable = {
params: SignTransferTxParams
result: SignTransferTxResult
}
alph_signAndSubmitMultiGroupTransferTx: {
params: SignTransferTxParams
result: SignTransferTxResult[]
}
alph_signAndSubmitDeployContractTx: {
params: SignDeployContractTxParams
result: SignDeployContractTxResult
Expand Down
3 changes: 3 additions & 0 deletions packages/walletconnect/test/shared/WalletClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 36 additions & 14 deletions packages/web3/src/api/api-alephium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1407,8 +1407,8 @@ export class HttpClient<SecurityDataType = unknown> {
property instanceof Blob
? property
: typeof property === 'object' && property !== null
? JSON.stringify(property)
: `${property}`
? JSON.stringify(property)
: `${property}`
)
return formData
}, new FormData()),
Expand Down Expand Up @@ -1488,18 +1488,18 @@ export class HttpClient<SecurityDataType = unknown> {
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)
Expand Down Expand Up @@ -2402,6 +2402,28 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
...params
}).then(convertHttpResponse),

/**
* No description
*
* @tags Transactions
* @name PostTransactionsBuild
* @summary Build as many unsigned transactions as many destinations from different groups is passed
* @request POST:/transactions/build-multi-group
*/
postTransactionsBuildMultiGroup: (data: BuildTransaction, params: RequestParams = {}) =>
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
*
Expand Down
25 changes: 25 additions & 0 deletions packages/web3/src/signer/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export abstract class SignerProvider {
}

abstract signAndSubmitTransferTx(params: SignTransferTxParams): Promise<SignTransferTxResult>
abstract signAndSubmitMultiGroupTransferTx(params: SignTransferTxParams): Promise<SignTransferTxResult[]>
abstract signAndSubmitDeployContractTx(params: SignDeployContractTxParams): Promise<SignDeployContractTxResult>
abstract signAndSubmitExecuteScriptTx(params: SignExecuteScriptTxParams): Promise<SignExecuteScriptTxResult>
abstract signAndSubmitUnsignedTx(params: SignUnsignedTxParams): Promise<SignUnsignedTxResult>
Expand Down Expand Up @@ -102,6 +103,13 @@ export abstract class SignerProviderSimple extends SignerProvider {
await this.submitTransaction(signResult)
return signResult
}
async signAndSubmitMultiGroupTransferTx(params: SignTransferTxParams): Promise<SignTransferTxResult[]> {
const signResults = await this.signMultiGroupTransferTx(params)
for (const signedTx of signResults) {
await this.submitTransaction(signedTx)
}
return signResults
}
async signAndSubmitDeployContractTx(params: SignDeployContractTxParams): Promise<SignDeployContractTxResult> {
const signResult = await this.signDeployContractTx(params)
await this.submitTransaction(signResult)
Expand Down Expand Up @@ -141,6 +149,23 @@ export abstract class SignerProviderSimple extends SignerProvider {
)
}

async signMultiGroupTransferTx(params: SignTransferTxParams): Promise<SignTransferTxResult[]> {
const results = await this.buildMultiGroupTransferTx(params)
const signedTxResults = await Promise.all(results.map(async (tx) => {

Check failure on line 154 in packages/web3/src/signer/signer.ts

View workflow job for this annotation

GitHub Actions / build (20)

Insert `⏎······`
const signature = await this.signRaw(params.signerAddress, tx.txId)

Check failure on line 155 in packages/web3/src/signer/signer.ts

View workflow job for this annotation

GitHub Actions / build (20)

Insert `··`
return { ...tx, signature }

Check failure on line 156 in packages/web3/src/signer/signer.ts

View workflow job for this annotation

GitHub Actions / build (20)

Insert `··`
}))

Check failure on line 157 in packages/web3/src/signer/signer.ts

View workflow job for this annotation

GitHub Actions / build (20)

Replace `})` with `··})⏎····`

return signedTxResults
}

async buildMultiGroupTransferTx(params: SignTransferTxParams): Promise<Omit<SignTransferTxResult, 'signature'>[]> {
return await TransactionBuilder.from(this.nodeProvider).buildMultiGroupTransferTx(
params,
await this.getPublicKey(params.signerAddress)
)
}

async signDeployContractTx(params: SignDeployContractTxParams): Promise<SignDeployContractTxResult> {
const response = await this.buildDeployContractTx(params)
const signature = await this.signRaw(params.signerAddress, response.txId)
Expand Down
22 changes: 22 additions & 0 deletions packages/web3/src/signer/tx-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,28 @@ export abstract class TransactionBuilder {
return { ...response, gasPrice: fromApiNumber256(response.gasPrice) }
}

async buildMultiGroupTransferTx(
params: SignTransferTxParams,
publicKey: string
): Promise<Omit<SignTransferTxResult, 'signature'>[]> {
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
Expand Down
118 changes: 118 additions & 0 deletions test/tipping-multi-group.test.ts
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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<string, number>

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 => ({

Check failure on line 91 in test/tipping-multi-group.test.ts

View workflow job for this annotation

GitHub Actions / build (20)

Replace `user` with `(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]])

Check failure on line 107 in test/tipping-multi-group.test.ts

View workflow job for this annotation

GitHub Actions / build (20)

Replace `['user1',·0.1],·['user2',·0.2]` with `⏎······['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)
})
})

0 comments on commit c011269

Please sign in to comment.