Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support pool reward tx #298

Merged
merged 4 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
154 changes: 37 additions & 117 deletions packages/web3/src/utils/exchange.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,12 @@ along with the library. If not, see <http://www.gnu.org/licenses/>.

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'

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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))
])
})
})
86 changes: 27 additions & 59 deletions packages/web3/src/utils/exchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ 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 { 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'

Expand All @@ -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<Address, bigint>()
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)
}
Expand Down Expand Up @@ -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 ||
Expand All @@ -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
}
8 changes: 1 addition & 7 deletions packages/web3/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Loading
Loading