diff --git a/packages/web3/src/utils/exchange.test.ts b/packages/web3/src/utils/exchange.test.ts index 0d3a0ec89..631ce4caf 100644 --- a/packages/web3/src/utils/exchange.test.ts +++ b/packages/web3/src/utils/exchange.test.ts @@ -18,14 +18,12 @@ along with the library. If not, see . import { PrivateKeyWallet } from '@alephium/web3-wallet' import { FixedAssetOutput, OutputRef, Transaction, UnsignedTx } from '../api/api-alephium' -import { DUST_AMOUNT, ONE_ALPH } from '../constants' import { getAddressFromUnlockScript, getSenderAddress, getALPHDepositInfo, - isSimpleALPHTransferTx, - isSimpleTransferTokenTx, - validateExchangeAddress + validateExchangeAddress, + isALPHTransferTx } from './exchange' import { NodeProvider } from '../api' @@ -88,8 +86,6 @@ describe('exchange', function () { const fromAddress = '1BPp69hdr78Fm6Qsh5N5FTmbgw5jEgg4P1K5oyvUBK8fw' const fromUnlockScript = '00023d7d9b04c6729c1e7ca27e08c295e3f45bdb5de9adcf2598b29c717595e7b1bf' - const invalidUnlockupScript = '0003498dc83e77e9b5c82b88e2bba7c737fd5aee041dc6bbb4402fefa3e7460a95bb' - const invalidToAddress = '18Y5mtrpu9kaEW9PoyipNQcFwVtA8X5yrGYhTZwYBwXHN' const outputRef: OutputRef = { hint: 0, key: '' } const outputTemplate: FixedAssetOutput = { hint: 0, @@ -128,56 +124,19 @@ describe('exchange', function () { } it('should validate deposit ALPH transaction', () => { - expect(isSimpleALPHTransferTx(txTemplate)).toEqual(true) + expect(isALPHTransferTx(txTemplate)).toEqual(true) expect(getSenderAddress(txTemplate)).toEqual(fromAddress) - expect(getALPHDepositInfo(txTemplate)).toEqual({ targetAddress: exchangeAddress, depositAmount: 10n }) + expect(getALPHDepositInfo(txTemplate)).toEqual([{ targetAddress: exchangeAddress, depositAmount: 10n }]) const tx0: Transaction = { ...txTemplate, unsigned: { ...unsignedTxTemplate, scriptOpt: '00112233' } } const tx1: Transaction = { ...txTemplate, contractInputs: [outputRef] } const tx2: Transaction = { ...txTemplate, generatedOutputs: [{ ...outputTemplate, type: 'AssetOutput' }] } const tx3: Transaction = { ...txTemplate, unsigned: { ...unsignedTxTemplate, inputs: [] } } - const invalidInput = { outputRef, unlockScript: invalidUnlockupScript } const tx4: Transaction = { ...txTemplate, - unsigned: { ...unsignedTxTemplate, inputs: [...unsignedTxTemplate.inputs, invalidInput] } + unsigned: { ...unsignedTxTemplate, fixedOutputs: [{ ...outputTemplate, tokens: [{ id: '00', amount: '10' }] }] } } - const invalidOutput1 = { ...outputTemplate, address: invalidToAddress } - const tx5: Transaction = { - ...txTemplate, - unsigned: { ...unsignedTxTemplate, fixedOutputs: [...unsignedTxTemplate.fixedOutputs, invalidOutput1] } - } - const invalidOutput2 = { ...outputTemplate, address: exchangeAddress, tokens: [{ id: '', amount: '10' }] } - const tx6: Transaction = { - ...txTemplate, - unsigned: { - ...unsignedTxTemplate, - fixedOutputs: [...unsignedTxTemplate.fixedOutputs.slice(0, -1), invalidOutput2] - } - } - const tx7: Transaction = { - ...txTemplate, - unsigned: { - ...unsignedTxTemplate, - inputs: [{ outputRef, unlockScript: exchangeUnlockScript }], - fixedOutputs: unsignedTxTemplate.fixedOutputs.slice(2) - } - } - const tx8: Transaction = { - ...txTemplate, - unsigned: { - ...unsignedTxTemplate, - fixedOutputs: [...unsignedTxTemplate.fixedOutputs.slice(2), invalidOutput1] - } - } - const tx9: Transaction = { - ...txTemplate, - unsigned: { - ...unsignedTxTemplate, - fixedOutputs: [...unsignedTxTemplate.fixedOutputs.slice(0, 2)] - } - } - const invalidTxs = [tx0, tx1, tx2, tx3, tx4, tx5, tx6, tx7, tx8, tx9] - invalidTxs.forEach((tx) => expect(isSimpleALPHTransferTx(tx)).toEqual(false)) + ;[tx0, tx1, tx2, tx3, tx4].forEach((tx) => expect(isALPHTransferTx(tx)).toEqual(false)) const multipleTargetAddressOutputTx: Transaction = { ...txTemplate, @@ -186,11 +145,12 @@ describe('exchange', function () { fixedOutputs: [...unsignedTxTemplate.fixedOutputs, { ...outputTemplate, address: exchangeAddress }] } } - expect(isSimpleALPHTransferTx(multipleTargetAddressOutputTx)).toEqual(true) - expect(getALPHDepositInfo(multipleTargetAddressOutputTx)).toEqual({ - targetAddress: exchangeAddress, - depositAmount: 20n - }) + expect(getALPHDepositInfo(multipleTargetAddressOutputTx)).toEqual([ + { + targetAddress: exchangeAddress, + depositAmount: 20n + } + ]) const sweepTx: Transaction = { ...txTemplate, @@ -199,76 +159,36 @@ describe('exchange', function () { fixedOutputs: [unsignedTxTemplate.fixedOutputs[2], { ...outputTemplate, address: exchangeAddress }] } } - expect(isSimpleALPHTransferTx(sweepTx)).toEqual(true) - expect(getALPHDepositInfo(sweepTx)).toEqual({ - targetAddress: exchangeAddress, - depositAmount: 20n - }) - }) - - it('should validate deposit token transaction', () => { - expect(isSimpleTransferTokenTx(txTemplate)).toEqual(false) - - const tokenId = '1a281053ba8601a658368594da034c2e99a0fb951b86498d05e76aedfe666800' - const exchangeTokenOutput: FixedAssetOutput = { - ...outputTemplate, - tokens: [{ id: tokenId, amount: '10' }], - address: exchangeAddress, - attoAlphAmount: DUST_AMOUNT.toString() - } - const tokenUnsignedTxTemplate = { - ...unsignedTxTemplate, - fixedOutputs: [...unsignedTxTemplate.fixedOutputs.slice(0, -1), exchangeTokenOutput] - } - const tokenTxTemplate: Transaction = { ...txTemplate, unsigned: tokenUnsignedTxTemplate } - expect(isSimpleTransferTokenTx(tokenTxTemplate)).toEqual(true) - - const tx0: Transaction = { ...tokenTxTemplate, unsigned: { ...tokenUnsignedTxTemplate, scriptOpt: '00112233' } } - const tx1: Transaction = { ...tokenTxTemplate, contractInputs: [outputRef] } - const tx2: Transaction = { ...tokenTxTemplate, generatedOutputs: [{ ...outputTemplate, type: 'AssetOutput' }] } - const tx3: Transaction = { ...tokenTxTemplate, unsigned: { ...tokenUnsignedTxTemplate, inputs: [] } } - const invalidInput = { outputRef, unlockScript: invalidUnlockupScript } - const tx4: Transaction = { - ...txTemplate, - unsigned: { ...tokenUnsignedTxTemplate, inputs: [...tokenUnsignedTxTemplate.inputs, invalidInput] } - } - const invalidOutput1 = { ...tokenUnsignedTxTemplate.fixedOutputs[2], address: invalidToAddress } - const tx5: Transaction = { - ...txTemplate, - unsigned: { ...tokenUnsignedTxTemplate, fixedOutputs: [...tokenUnsignedTxTemplate.fixedOutputs, invalidOutput1] } - } - const invalidOutput2 = { ...tokenUnsignedTxTemplate.fixedOutputs[2], attoAlphAmount: ONE_ALPH.toString() } - const tx6: Transaction = { - ...txTemplate, - unsigned: { - ...tokenUnsignedTxTemplate, - fixedOutputs: [...tokenUnsignedTxTemplate.fixedOutputs.slice(0, -1), invalidOutput2] + expect(getALPHDepositInfo(sweepTx)).toEqual([ + { + targetAddress: exchangeAddress, + depositAmount: 20n } - } - const tx7: Transaction = { - ...txTemplate, - unsigned: { - ...tokenUnsignedTxTemplate, - inputs: [{ outputRef, unlockScript: exchangeUnlockScript }], - fixedOutputs: tokenUnsignedTxTemplate.fixedOutputs.slice(2) - } - } - const tx8: Transaction = { + ]) + + const newAddress = PrivateKeyWallet.Random(undefined, new NodeProvider(''), 'default').address + const poolRewardTx: Transaction = { ...txTemplate, unsigned: { - ...tokenUnsignedTxTemplate, - fixedOutputs: [...tokenUnsignedTxTemplate.fixedOutputs.slice(2), invalidOutput1] + ...unsignedTxTemplate, + fixedOutputs: [ + ...unsignedTxTemplate.fixedOutputs, + { ...outputTemplate, address: exchangeAddress }, + { ...outputTemplate, address: newAddress }, + { ...outputTemplate, address: newAddress }, + { ...outputTemplate, address: newAddress } + ] } } - const tx9: Transaction = { - ...txTemplate, - unsigned: { - ...tokenUnsignedTxTemplate, - fixedOutputs: [...tokenUnsignedTxTemplate.fixedOutputs.slice(0, 2)] + expect(getALPHDepositInfo(poolRewardTx)).toEqual([ + { + targetAddress: exchangeAddress, + depositAmount: 20n + }, + { + targetAddress: newAddress, + depositAmount: 30n } - } - - const invalidTxs = [tx0, tx1, tx2, tx3, tx4, tx5, tx6, tx7, tx8, tx9] - invalidTxs.forEach((tx) => expect(isSimpleTransferTokenTx(tx)).toEqual(false)) + ]) }) }) diff --git a/packages/web3/src/utils/exchange.ts b/packages/web3/src/utils/exchange.ts index 62ba707af..58ccb4de3 100644 --- a/packages/web3/src/utils/exchange.ts +++ b/packages/web3/src/utils/exchange.ts @@ -16,7 +16,7 @@ You should have received a copy of the GNU Lesser General Public License along with the library. If not, see . */ -import { AddressType, DUST_AMOUNT, addressFromPublicKey, addressFromScript, binToHex, bs58, hexToBinUnsafe } from '..' +import { AddressType, addressFromPublicKey, addressFromScript, binToHex, bs58, hexToBinUnsafe } from '..' import { Transaction } from '../api/api-alephium' import { Address } from '../signer' @@ -37,34 +37,38 @@ export function validateExchangeAddress(address: string) { } } -export function isSimpleALPHTransferTx(tx: Transaction): boolean { - return isSimpleTransferTx(tx) && checkALPHOutput(tx) +export function isALPHTransferTx(tx: Transaction): boolean { + return isTransferTx(tx) && checkALPHOutput(tx) } -export function isSimpleTransferTokenTx(tx: Transaction): boolean { - const isTransferTx = isSimpleTransferTx(tx) - if (isTransferTx) { - const senderAddress = getSenderAddress(tx) - const targetAddress = tx.unsigned.fixedOutputs.find((o) => o.address !== senderAddress)!.address - return checkTokenOutput(tx, targetAddress) +export function getALPHDepositInfo(tx: Transaction): { targetAddress: Address; depositAmount: bigint }[] { + if (!isALPHTransferTx(tx)) { + return [] } - return false -} - -// we assume that the tx is a simple transfer tx, i.e. isSimpleTransferALPHTx(tx) == true -export function getALPHDepositInfo(tx: Transaction): { targetAddress: Address; depositAmount: bigint } { - const senderAddress = getSenderAddress(tx) - const targetAddress = tx.unsigned.fixedOutputs.find((o) => o.address !== senderAddress)!.address - let depositAmount = 0n + const inputAddresses: Address[] = [] + for (const input of tx.unsigned.inputs) { + try { + const address = getAddressFromUnlockScript(input.unlockScript) + if (!inputAddresses.includes(address)) { + inputAddresses.push(address) + } + } catch (_) {} + } + const result = new Map() tx.unsigned.fixedOutputs.forEach((o) => { - if (o.address === targetAddress) { - depositAmount += BigInt(o.attoAlphAmount) + if (!inputAddresses.includes(o.address)) { + const amount = result.get(o.address) + if (amount === undefined) { + result.set(o.address, BigInt(o.attoAlphAmount)) + } else { + result.set(o.address, BigInt(o.attoAlphAmount) + amount) + } } }) - return { targetAddress, depositAmount } + return Array.from(result.entries()).map(([key, value]) => ({ targetAddress: key, depositAmount: value })) } -// we assume that the tx is a simple transfer tx, i.e. isSimpleTransferALPHTx(tx) == true +// we assume that the tx is a simple transfer tx, i.e. isSimpleALPHTransferTx(tx) == true export function getSenderAddress(tx: Transaction): Address { return getAddressFromUnlockScript(tx.unsigned.inputs[0].unlockScript) } @@ -96,35 +100,12 @@ export function getAddressFromUnlockScript(unlockScript: string): Address { } } -function getSenderAddressAnyTx(tx: Transaction): Address | undefined { - try { - const inputAddresses = tx.unsigned.inputs.map((i) => getAddressFromUnlockScript(i.unlockScript)) - // we have checked that the inputs is not empty - const sender = inputAddresses[0] - return inputAddresses.slice(1).every((addr) => addr === sender) ? sender : undefined - } catch (_) { - return undefined - } -} - function checkALPHOutput(tx: Transaction): boolean { const outputs = tx.unsigned.fixedOutputs return outputs.every((o) => o.tokens.length === 0) } -function checkTokenOutput(tx: Transaction, to: Address): boolean { - // we have checked the output address - const outputs = tx.unsigned.fixedOutputs.filter((o) => o.address === to) - if (outputs[0].tokens.length === 0) { - return false - } - const tokenId = outputs[0].tokens[0].id - return outputs.every( - (o) => BigInt(o.attoAlphAmount) === DUST_AMOUNT && o.tokens.length === 1 && o.tokens[0].id === tokenId - ) -} - -function isSimpleTransferTx(tx: Transaction): boolean { +function isTransferTx(tx: Transaction): boolean { if ( tx.contractInputs.length !== 0 || tx.generatedOutputs.length !== 0 || @@ -133,18 +114,5 @@ function isSimpleTransferTx(tx: Transaction): boolean { ) { return false } - const sender = getSenderAddressAnyTx(tx) - if (sender === undefined) { - return false - } - const outputAddresses: Address[] = [] - tx.unsigned.fixedOutputs.forEach((o) => { - if (!outputAddresses.includes(o.address)) { - outputAddresses.push(o.address) - } - }) - return ( - (outputAddresses.length === 1 && outputAddresses[0] !== sender) || - (outputAddresses.length === 2 && outputAddresses.includes(sender)) - ) + return true } diff --git a/packages/web3/src/utils/index.ts b/packages/web3/src/utils/index.ts index 9d440afba..d11ecba64 100644 --- a/packages/web3/src/utils/index.ts +++ b/packages/web3/src/utils/index.ts @@ -24,10 +24,4 @@ export * from './utils' export * from './subscription' export * from './sign' export * from './number' -export { - validateExchangeAddress, - isSimpleALPHTransferTx, - isSimpleTransferTokenTx, - getSenderAddress, - getALPHDepositInfo -} from './exchange' +export { validateExchangeAddress, isALPHTransferTx, getSenderAddress, getALPHDepositInfo } from './exchange' diff --git a/test/exchange.test.ts b/test/exchange.test.ts index 129aa8bf5..d9dc2325e 100644 --- a/test/exchange.test.ts +++ b/test/exchange.test.ts @@ -17,25 +17,25 @@ along with the library. If not, see . */ import { PrivateKeyWallet, deriveHDWalletPrivateKey } from '@alephium/web3-wallet' -import { getSigners, transfer, testPrivateKey } from '@alephium/web3-test' +import { getSigners, transfer } from '@alephium/web3-test' import { Address, web3, ONE_ALPH, NodeProvider, - isSimpleALPHTransferTx, prettifyAttoAlphAmount, Subscription, node, sleep, TOTAL_NUMBER_OF_GROUPS, ALPH_TOKEN_ID, - getSenderAddress, - getALPHDepositInfo + getALPHDepositInfo, + groupOfAddress } from '@alephium/web3' import { waitTxConfirmed } from '@alephium/cli' import { EventEmitter } from 'stream' import * as bip39 from 'bip39' +import { testPrivateKey } from '@alephium/web3-test' const WithdrawFee = ONE_ALPH @@ -188,30 +188,29 @@ class Exchange { this.balances = new Map() } - async handleDepositTx(tx: node.Transaction, depositAddress: Address, depositAmount: bigint) { + async handleDepositInfo(depositAddress: Address, depositAmount: bigint) { const pathIndex = this.getPathIndex(depositAddress) const wallet = this.getWalletByPathIndex(pathIndex) const sweepTxIds = await sweep(wallet, this.wallet.address) await waitTxsConfirmed(sweepTxIds) this.sweepTxs.push(...sweepTxIds) - const sender = getSenderAddress(tx) - const userBalance = this.balances.get(sender) + const userBalance = this.balances.get(depositAddress) if (userBalance === undefined) { - this.balances.set(sender, depositAmount) + this.balances.set(depositAddress, depositAmount) } else { - this.balances.set(sender, userBalance + depositAmount) + this.balances.set(depositAddress, userBalance + depositAmount) } - this.depositTxs.push(tx.unsigned.txId) } async handleBlock(block: node.BlockEntry, resolver: () => void) { for (const tx of block.transactions) { - if (isSimpleALPHTransferTx(tx)) { - const { targetAddress, depositAmount } = getALPHDepositInfo(tx) - if (this.hotAddresses.includes(targetAddress)) { - await this.handleDepositTx(tx, targetAddress, depositAmount) + const infos = getALPHDepositInfo(tx).filter((v) => this.hotAddresses.includes(v.targetAddress)) + if (infos.length > 0) { + for (const { targetAddress, depositAmount } of infos) { + await this.handleDepositInfo(targetAddress, depositAmount) } + this.depositTxs.push(tx.unsigned.txId) } } resolver() @@ -264,7 +263,7 @@ class Exchange { async withdraw(user: User, amount: bigint) { console.log(`withdraw ${prettifyAttoAlphAmount(amount)} to ${user.address}`) - const balance = this.getBalance(user.address) + const balance = this.getBalance(user.depositAddress) if (balance < amount + WithdrawFee) { throw new Error('Not enough balance') } @@ -273,9 +272,9 @@ class Exchange { this.withdrawTxs.push(result.txId) const remain = balance - (amount + WithdrawFee) if (remain === 0n) { - this.balances.delete(user.address) + this.balances.delete(user.depositAddress) } else { - this.balances.set(user.address, remain) + this.balances.set(user.depositAddress, remain) } } @@ -336,31 +335,58 @@ describe('exchange', function () { await waitTxsConfirmed(results0.map((result) => result.txId)) } - const totalTxNumber = depositTimes * userNumPerGroup * TOTAL_NUMBER_OF_GROUPS - async function waitForCollectTxs() { + const depositTxNumber = depositTimes * userNumPerGroup * TOTAL_NUMBER_OF_GROUPS + async function waitForCollectTxs(txNumber: number) { const depositTxs = exchange.getDepositTxs() - if (depositTxs.length < totalTxNumber) { + if (depositTxs.length < txNumber) { await sleep(1000) - await waitForCollectTxs() + await waitForCollectTxs(txNumber) } return } - await waitForCollectTxs() // check deposit txs + await waitForCollectTxs(depositTxNumber) console.log(`checking deposit txs...`) - const depositTxs = exchange.getDepositTxs() - expect(depositTxs.length).toEqual(totalTxNumber) + const depositTxs0 = exchange.getDepositTxs() + expect(depositTxs0.length).toEqual(depositTxNumber) + + const testWallet = new PrivateKeyWallet({ privateKey: testPrivateKey }) + const poolReward = ONE_ALPH + let poolRewardTxNumber = 0 + for (let i = 0; i < TOTAL_NUMBER_OF_GROUPS; i++) { + console.log(`pool reward tx, group: ${i}`) + const destinations = users + .filter((u) => groupOfAddress(u.depositAddress) === i) + .map((u) => ({ + address: u.depositAddress, + attoAlphAmount: ONE_ALPH.toString() + })) + if (destinations.length > 0) { + const result = await testWallet.signAndSubmitTransferTx({ + signerAddress: testWallet.address, + destinations + }) + await waitTxConfirmed(nodeProvider, result.txId, 1, 1000) + poolRewardTxNumber += 1 + } + } + + // check pool reward txs + console.log(`checking pool reward txs...`) + await waitForCollectTxs(depositTxNumber + poolRewardTxNumber) + const depositTxs1 = exchange.getDepositTxs() + expect(depositTxs1.length).toEqual(depositTxNumber + poolRewardTxNumber) // check balances console.log(`checking balances...`) let totalDepositAmount = 0n for (const user of users) { - const depositAmount = exchange.getBalance(user.address) + const depositAmount = exchange.getBalance(user.depositAddress) totalDepositAmount += depositAmount const gasFee = await user.getDepositGasFee() const userBalance = await nodeProvider.addresses.getAddressesAddressBalance(user.address) - expect(BigInt(userBalance.balance)).toEqual(initialBalance - depositAmount - gasFee) + expect(BigInt(userBalance.balance)).toEqual(initialBalance - depositAmount - gasFee + poolReward) } const sweepTxFee = await getGasFee(exchange.getSweepTxs()) const exchangeBalance0 = await nodeProvider.addresses.getAddressesAddressBalance(exchange.wallet.address) @@ -372,22 +398,24 @@ describe('exchange', function () { for (let index = 0; index < withdrawTimes - 1; index++) { for (const user of users) { await exchange.withdraw(user, ONE_ALPH) - const balanceInExchange = exchange.getBalance(user.address) + const balanceInExchange = exchange.getBalance(user.depositAddress) const gasFee = await user.getDepositGasFee() const userBalance = await nodeProvider.addresses.getAddressesAddressBalance(user.address) expect(BigInt(userBalance.balance)).toEqual( - initialBalance - balanceInExchange - gasFee - WithdrawFee * BigInt(index + 1) + initialBalance - balanceInExchange - gasFee - WithdrawFee * BigInt(index + 1) + poolReward ) } } // withdraw remain balances for (const user of users) { - const balance = exchange.getBalance(user.address) + const balance = exchange.getBalance(user.depositAddress) await exchange.withdraw(user, balance - WithdrawFee) const userBalance = await nodeProvider.addresses.getAddressesAddressBalance(user.address) const gasFee = await user.getDepositGasFee() - expect(BigInt(userBalance.balance)).toEqual(initialBalance - gasFee - WithdrawFee * BigInt(withdrawTimes)) + expect(BigInt(userBalance.balance)).toEqual( + initialBalance - gasFee - WithdrawFee * BigInt(withdrawTimes) + poolReward + ) } const withdrawGasFee = await getGasFee(exchange.getWithdrawTxs())