Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c011269
mutli-group-transfer
pragmaxim Sep 20, 2024
0cd8a3d
fix linter errors
pragmaxim Sep 20, 2024
63f205a
rename-to-lending-test
pragmaxim Sep 23, 2024
49efee6
track request time
pragmaxim Sep 24, 2024
125cd49
cleanup tracking
pragmaxim Sep 24, 2024
6b127ce
avoid changing signer interface for multi-group feature
pragmaxim Sep 26, 2024
544284a
multi-group endpoint openapi name fix
pragmaxim Sep 26, 2024
2729e03
Distribute wealth in lending bot
pragmaxim Oct 4, 2024
9feb212
generate wallet per user in lending bot
pragmaxim Oct 6, 2024
0efe700
multi-group spec
pragmaxim Oct 7, 2024
23a720d
docker image with dev-alephium multi-group support
pragmaxim Oct 7, 2024
00596a7
Transaction test fix
pragmaxim Oct 7, 2024
aae1c93
Merge branch 'master' into multi-group-transfer
pragmaxim Oct 7, 2024
f1cad93
using alephium docker image with multi-group feature
pragmaxim Oct 7, 2024
68c694c
fixing racing condition
pragmaxim Oct 8, 2024
0748c1c
lint:fix lending and transaction tests
pragmaxim Oct 8, 2024
bdc704c
cleanup after merging master
pragmaxim Oct 8, 2024
b881c41
update schemas after merging master
pragmaxim Oct 8, 2024
b1e953d
Merge branch 'master' into multi-group-transfer
pragmaxim Oct 28, 2024
c7403b7
Merge branch 'master' into multi-group-transfer
pragmaxim Nov 5, 2024
85a0835
fixes after merging new features
pragmaxim Nov 5, 2024
2efbdf7
removing test.only modifier
pragmaxim Nov 6, 2024
d241853
rename multi-transfer to transfer-from-one-to-many-groups
pragmaxim Nov 22, 2024
5c0a77e
Merge branch 'master' into multi-group-transfer
pragmaxim Nov 22, 2024
76fd33c
upgrade to 3.9.0
pragmaxim Nov 22, 2024
45b9e7b
docker image version fix
pragmaxim Nov 22, 2024
1270de4
Merge branch 'master' into multi-group-transfer
pragmaxim Nov 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/web3-wallet/src/privatekey-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class PrivateKeyWallet extends SignerProviderSimple {
return Promise.resolve(this.account)
}

protected getPublicKey(address: string): Promise<string> {
getPublicKey(address: string): Promise<string> {
if (address !== this.address) {
throw Error('The signer address is invalid')
}
Expand Down
2 changes: 1 addition & 1 deletion packages/web3/src/signer/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export abstract class SignerProviderSimple extends SignerProvider {
return signResults
}

protected abstract getPublicKey(address: string): Promise<string>
abstract getPublicKey(address: string): Promise<string>
Copy link
Author

@pragmaxim pragmaxim Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please let me know if the SignerProviderSimple can have this method public. Not important much, it would be easier to have a single method for build+sign+submit with just a PrivateKeyWallet


async signTransferTx(params: SignTransferTxParams): Promise<SignTransferTxResult> {
const response = await this.buildTransferTx(params)
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 @@ -72,6 +72,28 @@ export abstract class TransactionBuilder {
return this.convertTransferTxResult(response)
}

async buildTransferFromOneToManyGroups(
params: SignTransferTxParams,
publicKey: string
): Promise<Omit<SignTransferTxResult, 'signature'>[]> {
TransactionBuilder.validatePublicKey(params, publicKey, params.signerKeyType)

const { destinations, gasPrice, ...rest } = params
const data: node.BuildTransferTx = {
fromPublicKey: publicKey,
fromPublicKeyType: params.signerKeyType,
destinations: toApiDestinations(destinations),
gasPrice: toApiNumber256Optional(gasPrice),
...rest
}
const results = await this.nodeProvider.transactions.postTransactionsBuildTransferFromOneToManyGroups(data)
const response = results.map((result) => ({
...result,
gasPrice: fromApiNumber256(result.gasPrice)
}))
return response
}

async buildDeployContractTx(
params: SignDeployContractTxParams,
publicKey: string
Expand Down
200 changes: 200 additions & 0 deletions test/lending.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
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 {
convertAlphAmountWithDecimals,
DEFAULT_GAS_ALPH_AMOUNT,
NodeProvider,
number256ToNumber,
ONE_ALPH,
SignerProviderSimple,
SignTransferTxParams,
SignTransferTxResult,
TransactionBuilder
} from '@alephium/web3'
import { testNodeWallet } from '@alephium/web3-test'
import { PrivateKeyWallet, deriveHDWalletPrivateKeyForGroup } from '@alephium/web3-wallet'

class LendingBot {
private readonly nodeProvider: NodeProvider // This can be initialized with node url + api key in a real application
readonly userGroups: Map<string, number>
readonly userWallets: Map<string, PrivateKeyWallet>

constructor(nodeProvider: NodeProvider) {
this.nodeProvider = nodeProvider
this.userGroups = new Map()
this.userWallets = new Map()
}

addUser(userId: string): PrivateKeyWallet {
if (this.userGroups.has(userId)) {
throw new Error(`User ${userId} already exists`)
}

const groupNumber = this.userGroups.size
const wallet = PrivateKeyWallet.Random(groupNumber, this.nodeProvider)
this.userGroups.set(userId, groupNumber)
this.userWallets.set(userId, wallet)
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 wallet = this.userWallets.get(userId)
if (wallet === undefined) {
throw new Error(`User ${userId} wallet does not exist`)
}
return wallet as PrivateKeyWallet
}

getUserAddress(userId: string) {
return this.getUserWallet(userId).address
}

async signAndSubmitTransferFromOneToManyGroups(
signer: SignerProviderSimple,
params: SignTransferTxParams
): Promise<SignTransferTxResult[]> {
const buildTxResults = await TransactionBuilder.from(this.nodeProvider).buildTransferFromOneToManyGroups(
params,
await signer.getPublicKey(params.signerAddress)
)
const results: SignTransferTxResult[] = []
for (const tx of buildTxResults) {
const result = await signer.signAndSubmitUnsignedTx({
signerAddress: params.signerAddress,
unsignedTx: tx.unsignedTx
})
results.push(result)
}
return results
}

async getUserBalance(userId: string) {
const userWallet = this.getUserWallet(userId)
const balance = await userWallet.nodeProvider.addresses.getAddressesAddressBalance(userWallet.address)
return number256ToNumber(balance.balance, 18)
}

async distributeWealth(users: string[], deposit: bigint) {
const signer = await testNodeWallet()
const signerAddress = (await signer.getSelectedAccount()).address

const destinations = users.map((user) => ({
address: this.addUser(user).address,
attoAlphAmount: deposit
}))

await this.signAndSubmitTransferFromOneToManyGroups(signer, {
signerAddress,
destinations
})
}

async transfer(fromUserId: string, toUserData: [string, number][]) {
const signer = this.getUserWallet(fromUserId)
const signerAddress = signer.address

const destinations = toUserData.map(([user, amount]) => ({
address: this.getUserAddress(user),
attoAlphAmount: convertAlphAmountWithDecimals(amount)!
}))

await this.signAndSubmitTransferFromOneToManyGroups(signer, {
signerAddress,
destinations
})
}
}

jest.setTimeout(15_000)

async function track<T>(label: string, fn: () => Promise<T>): Promise<T> {
const start = Date.now()
const result = await fn()
const end = Date.now()
console.log(`${label} completed in ${end - start} milliseconds`)
return result
}

describe('lendingbot', function () {
it('should work', async function () {
const lendingBot = new LendingBot(new NodeProvider('http://127.0.0.1:22973'))

// each user will start with 1 ALPH
const users = ['user0', 'user1', 'user2']
const deposit = ONE_ALPH

await track('Distributing alphs among users', async () => {
await lendingBot.distributeWealth(users, deposit)
})

await track('Check user balances', async () => {
for (const user of users) {
const balance = await lendingBot.getUserBalance(user)
expect(balance).toEqual(1.0)
}
})

await track('user0 lends to user1 and user2', async () => {
await lendingBot.transfer('user0', [
['user1', 0.1],
['user2', 0.2]
])
})

await track('user1 lends to user2', async () => {
await lendingBot.transfer('user1', [['user2', 0.3]])
})

await track('Check user balances', async () => {
const balance0 = await lendingBot.getUserBalance('user0')
const balance1 = await lendingBot.getUserBalance('user1')
const balance2 = await lendingBot.getUserBalance('user2')

expect(balance0).toEqual(1.0 - 0.1 - 0.2 - DEFAULT_GAS_ALPH_AMOUNT * 2)
expect(balance1).toEqual(1.0 + 0.1 - 0.3 - DEFAULT_GAS_ALPH_AMOUNT)
expect(balance2).toEqual(1.0 + 0.2 + 0.3)
})

await track('user1 returns to user0', async () => {
await lendingBot.transfer('user1', [['user0', 0.1]])
})

await track('user2 returns to user0 and user1', async () => {
await lendingBot.transfer('user2', [
['user0', 0.2],
['user1', 0.3]
])
})

await track('Check user balances', async () => {
const finalBalance0 = await lendingBot.getUserBalance('user0')
const finalBalance1 = await lendingBot.getUserBalance('user1')
const finalBalance2 = await lendingBot.getUserBalance('user2')
expect(finalBalance0).toEqual(1.0 - 0.1 - 0.2 + 0.1 + 0.2 - DEFAULT_GAS_ALPH_AMOUNT * 2)
expect(finalBalance1).toEqual(1.0 + 0.1 - 0.3 - 0.1 + 0.3 - DEFAULT_GAS_ALPH_AMOUNT * 2)
expect(finalBalance2).toEqual(1.0 + 0.2 + 0.3 - 0.2 - 0.3 - DEFAULT_GAS_ALPH_AMOUNT * 2)

console.log('Final balances', { finalBalance0, finalBalance1, finalBalance2 })
})
})
})
109 changes: 106 additions & 3 deletions test/transaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,37 @@ 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 { DappTransactionBuilder, SignTransferChainedTxParams, subscribeToTxStatus } from '../packages/web3'
import { SignTransferTxResult, SignTransferChainedTxParams, SignUnsignedTxResult } from '../packages/web3'
import { node, ONE_ALPH, DUST_AMOUNT, MINIMAL_CONTRACT_DEPOSIT } from '../packages/web3'
import { SubscribeOptions, sleep } from '../packages/web3'
import { subscribeToTxStatus, SubscribeOptions, sleep } from '../packages/web3'
import { web3 } from '../packages/web3'
import { TxStatus } from '../packages/web3'
import { HDWallet, HDWalletAccount, PrivateKeyWallet, generateMnemonic } from '@alephium/web3-wallet'
import { Add, Sub, AddMain, Transact, Deposit, DepositToken } from '../artifacts/ts'
import { getSigner, mintToken, testPrivateKeyWallet } from '../packages/web3-test'
import { TransactionBuilder } from '../packages/web3'
import { DappTransactionBuilder, TransactionBuilder } from '../packages/web3'
import { ALPH_TOKEN_ID } from '../packages/web3'
import { Balance } from '@alephium/web3/src/api/api-alephium'

jest.setTimeout(10_000)

async function signAndSubmitTransactions(
transactions: Omit<SignTransferTxResult, 'signature'>[],
signer: PrivateKeyWallet
): Promise<SignUnsignedTxResult[]> {
const signedResults: SignTransferTxResult[] = []

for (const tx of transactions) {
const result = await signer.signAndSubmitUnsignedTx({
signerAddress: signer.address,
unsignedTx: tx.unsignedTx
})
signedResults.push(result)
}

return signedResults
}

describe('transactions', function () {
let signer: PrivateKeyWallet

Expand All @@ -36,6 +55,90 @@ describe('transactions', function () {
signer = await getSigner()
})

it('should build transfer-from-one-to-many-groups', async () => {
const nodeProvider = web3.getCurrentNodeProvider()
const signer0 = await getSigner(100n * ONE_ALPH, 1)
const signer1 = await getSigner(0n, 2)
const signer2 = await getSigner(0n, 3)
const signer3 = await getSigner(0n, 3)
const signer4 = await getSigner(0n, 0)

const signer0Balance = await nodeProvider.addresses.getAddressesAddressBalance(signer0.address)
expect(BigInt(signer0Balance.balance)).toBe(100n * ONE_ALPH)

const transferFrom0to1and2 = await TransactionBuilder.from(nodeProvider).buildTransferFromOneToManyGroups(
{
signerAddress: signer0.address,
destinations: [signer1, signer2].map((signer) => ({
address: signer.address,
attoAlphAmount: 10n * ONE_ALPH
}))
},
await signer0.getPublicKey(signer0.address)
)

const transferFrom0to1and2Result = await signAndSubmitTransactions(transferFrom0to1and2, signer0)

const signer1Balance = await nodeProvider.addresses.getAddressesAddressBalance(signer1.address)
expect(BigInt(signer1Balance.balance)).toBe(10n * ONE_ALPH)
const signer2Balance = await nodeProvider.addresses.getAddressesAddressBalance(signer2.address)
expect(BigInt(signer2Balance.balance)).toBe(10n * ONE_ALPH)

const transferFrom1to3and4 = await TransactionBuilder.from(nodeProvider).buildTransferFromOneToManyGroups(
{
signerAddress: signer1.address,
destinations: [signer3, signer4].map((signer) => ({
address: signer.address,
attoAlphAmount: ONE_ALPH
}))
},
signer1.publicKey
)

const transferFrom1to3and4Result = await signAndSubmitTransactions(transferFrom1to3and4, signer1)

const transferFrom2to3and4 = await TransactionBuilder.from(nodeProvider).buildTransferFromOneToManyGroups(
{
signerAddress: signer2.address,
destinations: [signer3, signer4].map((signer) => ({
address: signer.address,
attoAlphAmount: ONE_ALPH
}))
},
signer2.publicKey
)

const transferFrom2to3and4Result = await signAndSubmitTransactions(transferFrom2to3and4, signer2)

const signer0FinalBalance = await nodeProvider.addresses.getAddressesAddressBalance(signer0.address)
const signer1FinalBalance = await nodeProvider.addresses.getAddressesAddressBalance(signer1.address)
const signer2FinalBalance = await nodeProvider.addresses.getAddressesAddressBalance(signer2.address)
const signer3FinalBalance = await nodeProvider.addresses.getAddressesAddressBalance(signer3.address)
const signer4FinalBalance = await nodeProvider.addresses.getAddressesAddressBalance(signer4.address)

const gasCostTransferFrom0to1and2 = transferFrom0to1and2Result.reduce(
(sum, item) => sum + BigInt(item.gasAmount) * BigInt(item.gasPrice),
BigInt(0)
)
const gasCostTransferFrom1to3and4 = transferFrom1to3and4Result.reduce(
(sum, item) => sum + BigInt(item.gasAmount) * BigInt(item.gasPrice),
BigInt(0)
)
const gasCostTransferFrom2to3and4 = transferFrom2to3and4Result.reduce(
(sum, item) => sum + BigInt(item.gasAmount) * BigInt(item.gasPrice),
BigInt(0)
)
const expectedSigner0Balance = 100n * ONE_ALPH - 20n * ONE_ALPH - gasCostTransferFrom0to1and2
const expectedSigner1Balance = 10n * ONE_ALPH - 2n * ONE_ALPH - gasCostTransferFrom1to3and4
const expectedSigner2Balance = 10n * ONE_ALPH - 2n * ONE_ALPH - gasCostTransferFrom2to3and4

expect(BigInt(signer0FinalBalance.balance)).toBe(expectedSigner0Balance)
expect(BigInt(signer1FinalBalance.balance)).toBe(expectedSigner1Balance)
expect(BigInt(signer2FinalBalance.balance)).toBe(expectedSigner2Balance)
expect(BigInt(signer3FinalBalance.balance)).toBe(2n * ONE_ALPH)
expect(BigInt(signer4FinalBalance.balance)).toBe(2n * ONE_ALPH)
})

it('should subscribe transaction status', async () => {
const sub = Sub.contract
const txParams = await sub.txParamsForDeployment(signer, {
Expand Down
Loading