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())