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

Add some util functions for the exchange #277

Merged
merged 7 commits into from
Dec 4, 2023
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"trailingComma": "none"
},
"engines": {
"node": ">=16.0.0",
"node": ">=14.0.0",
"npm": ">=7.0.0",
"pnpm": ">=7.0.0"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"typescript": "4.7.4"
},
"engines": {
"node": ">=16.0.0",
"node": ">=14.0.0",
"npm": ">=7.0.0"
}
}
2 changes: 1 addition & 1 deletion packages/walletconnect/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"webpack-cli": "4.10.0"
},
"engines": {
"node": ">=16.0.0",
"node": ">=14.0.0",
"npm": ">=7.0.0"
}
}
2 changes: 1 addition & 1 deletion packages/web3-test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"crypto-browserify": "^3.12.0"
},
"engines": {
"node": ">=16.0.0",
"node": ">=14.0.0",
"npm": ">=7.0.0"
}
}
2 changes: 1 addition & 1 deletion packages/web3-wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"crypto-browserify": "^3.12.0"
},
"engines": {
"node": ">=16.0.0",
"node": ">=14.0.0",
"npm": ">=7.0.0"
}
}
2 changes: 1 addition & 1 deletion packages/web3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"webpack-cli": "^4.10.0"
},
"engines": {
"node": ">=16.0.0",
"node": ">=14.0.0",
"npm": ">=7.0.0"
}
}
192 changes: 192 additions & 0 deletions packages/web3/src/utils/exchange.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
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 { FixedAssetOutput, OutputRef, Transaction, UnsignedTx } from '../api/api-alephium'
import { DUST_AMOUNT, ONE_ALPH } from '../constants'
import {
getAddressFromUnlockScript,
isDepositALPHTransaction,
isDepositTokenTransaction,
isExchangeAddress
} from './exchange'

describe('exchange', function () {
it('should get address from unlock script', () => {
expect(getAddressFromUnlockScript('0003498dc83e77e9b5c82b88e2bba7c737fd5aee041dc6bbb4402fefa3e7460a95bb')).toEqual(
'18Y5mtrpu9kaEW9PoyipNQcFwVtA8X5yrGYhTZwYBwXHN'
)

expect(
getAddressFromUnlockScript(
'0201010000000004581440200000000000000000000000000000000000000000000000000000000000000000868500'
)
).toEqual('qCG5ZXg3b7AuGDS4HoEAhzqhCc2yxMqBYjYimBj1QFFT')

expect(() =>
getAddressFromUnlockScript(
'01024437e4e935b8c50558c361ec99255bf393bf9862af7a3c24a813ae155284211f4a01c3c6f3f8614f159e7373f4837ef95432c82dc47a5489df50046e7e7e3dcb227db403'
)
).toThrow('Naive multi-sig address is not supported for exchanges as it will be replaced by P2SH')

expect(() => getAddressFromUnlockScript('')).toThrow('UnlockScript is empty')
expect(() => getAddressFromUnlockScript('030011223344')).toThrow('Invalid unlock script type')
})

it('should validate exchange address', () => {
expect(isExchangeAddress('18Y5mtrpu9kaEW9PoyipNQcFwVtA8X5yrGYhTZwYBwXHN')).toEqual(true)
expect(isExchangeAddress('qCG5ZXg3b7AuGDS4HoEAhzqhCc2yxMqBYjYimBj1QFFT')).toEqual(true)
expect(isExchangeAddress('22sTaM5xer7h81LzaGA2JiajRwHwECpAv9bBuFUH5rrnr')).toEqual(false)
expect(
isExchangeAddress(
'X3RMnvb8h3RFrrbBraEouAWU9Ufu4s2WTXUQfLCvDtcmqCWRwkVLc69q2NnwYW2EMwg4QBN2UopkEmYLLLgHP9TQ38FK15RnhhEwguRyY6qCuAoRfyjHRnqYnTvfypPgD7w1ku'
)
).toEqual(false)
expect(isExchangeAddress('18Y5mtrpu9kaEW9PoyipNQcFwVtA8X5yrGYhTZwYBw')).toEqual(false)
expect(isExchangeAddress('qCG5ZXg3b7AuGDS4HoEAhzqhCc2yxMqBYjYimBj1Q')).toEqual(false)
expect(() => isExchangeAddress('')).toThrow('Address is empty')
expect(() => isExchangeAddress('6aac0693404223ed9c492bc61fd3cbf9')).toThrow('Non-base58 character')
expect(() => isExchangeAddress('I8Y5mtrpu9kaEW9PoyipNQcFwVtA8X5yrGYhTZwYBwXHN')).toThrow('Non-base58 character')
})

const exchangeAddress = '13ausZBtpjsZ87zB3iUZajSwX9CdcVUariz1Q8K2j7tNV'
const fromAddress = '1BPp69hdr78Fm6Qsh5N5FTmbgw5jEgg4P1K5oyvUBK8fw'
const fromUnlockScript = '00023d7d9b04c6729c1e7ca27e08c295e3f45bdb5de9adcf2598b29c717595e7b1bf'
const invalidUnlockupScript = '0003498dc83e77e9b5c82b88e2bba7c737fd5aee041dc6bbb4402fefa3e7460a95bb'
const invalidToAddress = '18Y5mtrpu9kaEW9PoyipNQcFwVtA8X5yrGYhTZwYBwXHN'
const outputRef: OutputRef = { hint: 0, key: '' }
const outputTemplate: FixedAssetOutput = {
hint: 0,
key: '',
lockTime: 0,
message: '',
attoAlphAmount: '0',
address: '',
tokens: []
}

const unsignedTxTemplate: UnsignedTx = {
txId: '6aac0693404223ed9c492bc61fd3cbf99f36927925bb2f3dc237fcf92b39d684',
version: 0,
networkId: 0,
gasAmount: 78580,
gasPrice: '100000000000',
inputs: [
{ outputRef, unlockScript: fromUnlockScript },
{ outputRef, unlockScript: fromUnlockScript },
{ outputRef, unlockScript: fromUnlockScript }
],
fixedOutputs: [
{ ...outputTemplate, address: fromAddress },
{ ...outputTemplate, address: fromAddress },
{ ...outputTemplate, address: exchangeAddress }
]
}
const txTemplate: Transaction = {
unsigned: unsignedTxTemplate,
scriptExecutionOk: true,
contractInputs: [],
generatedOutputs: [],
inputSignatures: [],
scriptSignatures: []
}

it('should validate deposit ALPH transaction', () => {
expect(isDepositALPHTransaction(txTemplate, exchangeAddress)).toEqual(true)
expect(isDepositALPHTransaction(txTemplate, invalidToAddress)).toEqual(false)

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] }
}
const invalidOutput0 = { ...outputTemplate, address: exchangeAddress }
const tx5: Transaction = {
...txTemplate,
unsigned: { ...unsignedTxTemplate, fixedOutputs: [...unsignedTxTemplate.fixedOutputs, invalidOutput0] }
}
const invalidOutput1 = { ...outputTemplate, address: invalidToAddress }
const tx6: Transaction = {
...txTemplate,
unsigned: { ...unsignedTxTemplate, fixedOutputs: [...unsignedTxTemplate.fixedOutputs, invalidOutput1] }
}
const invalidOutput2 = { ...outputTemplate, address: exchangeAddress, tokens: [{ id: '', amount: '10' }] }
const tx7: Transaction = {
...txTemplate,
unsigned: {
...unsignedTxTemplate,
fixedOutputs: [...unsignedTxTemplate.fixedOutputs.slice(0, -1), invalidOutput2]
}
}
const invalidTxs = [tx0, tx1, tx2, tx3, tx4, tx5, tx6, tx7]
invalidTxs.forEach((tx) => expect(isDepositALPHTransaction(tx, exchangeAddress)).toEqual(false))
})

it('should validate deposit token transaction', () => {
expect(isDepositTokenTransaction(txTemplate, exchangeAddress)).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(isDepositTokenTransaction(tokenTxTemplate, exchangeAddress)).toEqual(true)
expect(isDepositTokenTransaction(tokenTxTemplate, invalidToAddress)).toEqual(false)

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 invalidOutput0 = { ...outputTemplate, address: exchangeAddress }
const tx5: Transaction = {
...txTemplate,
unsigned: { ...tokenUnsignedTxTemplate, fixedOutputs: [...tokenUnsignedTxTemplate.fixedOutputs, invalidOutput0] }
}
const invalidOutput1 = { ...tokenUnsignedTxTemplate.fixedOutputs[2], address: invalidToAddress }
const tx6: Transaction = {
...txTemplate,
unsigned: { ...tokenUnsignedTxTemplate, fixedOutputs: [...tokenUnsignedTxTemplate.fixedOutputs, invalidOutput1] }
}
const invalidOutput2 = { ...tokenUnsignedTxTemplate.fixedOutputs[2], attoAlphAmount: ONE_ALPH.toString() }
const tx7: Transaction = {
...txTemplate,
unsigned: {
...tokenUnsignedTxTemplate,
fixedOutputs: [...tokenUnsignedTxTemplate.fixedOutputs.slice(0, -1), invalidOutput2]
}
}

const invalidTxs = [tx0, tx1, tx2, tx3, tx4, tx5, tx6, tx7]
invalidTxs.forEach((tx) => expect(isDepositTokenTransaction(tx, exchangeAddress)).toEqual(false))
})
})
120 changes: 120 additions & 0 deletions packages/web3/src/utils/exchange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
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 { AddressType, DUST_AMOUNT, addressFromPublicKey, addressFromScript, binToHex, bs58, hexToBinUnsafe } from '..'
import { Transaction } from '../api/api-alephium'
import { Address } from '../signer'

export function isExchangeAddress(address: string): boolean {
const decoded = bs58.decode(address)
if (decoded.length === 0) throw new Error('Address is empty')
const addressType = decoded[0]
return (addressType === AddressType.P2PKH || addressType === AddressType.P2SH) && decoded.length === 33
}

export function isDepositALPHTransaction(tx: Transaction, exchangeAddress: string): boolean {
return isDepositTransaction(tx, exchangeAddress) && checkALPHOutput(tx)
}

export function isDepositTokenTransaction(tx: Transaction, exchangeAddress: string): boolean {
return isDepositTransaction(tx, exchangeAddress) && checkTokenOutput(tx, exchangeAddress)
}

// we assume that the tx is deposit transaction
export function getDepositAddress(tx: Transaction): Address {
return getAddressFromUnlockScript(tx.unsigned.inputs[0].unlockScript)
}

enum UnlockScriptType {
P2PKH = 0x00,
P2MPKH = 0x01,
P2SH = 0x02
}

export function getAddressFromUnlockScript(unlockScript: string): Address {
polarker marked this conversation as resolved.
Show resolved Hide resolved
const decoded = hexToBinUnsafe(unlockScript)
if (decoded.length === 0) throw new Error('UnlockScript is empty')
const unlockScriptType = decoded[0]
const unlockScriptBody = decoded.slice(1)

if (unlockScriptType === UnlockScriptType.P2PKH) {
return addressFromPublicKey(binToHex(unlockScriptBody))
} else if (unlockScriptType === UnlockScriptType.P2MPKH) {
throw new Error('Naive multi-sig address is not supported for exchanges as it will be replaced by P2SH')
} else if (unlockScriptType === UnlockScriptType.P2SH) {
// FIXEME: for now we assume that the params is empty, so we need to
Lbqds marked this conversation as resolved.
Show resolved Hide resolved
// remove the last byte from the `unlockScriptBody`, we can decode
// the unlock script once the codec PR is merged
const script = unlockScriptBody.slice(0, -1)
return addressFromScript(script)
} else {
throw new Error('Invalid unlock script type')
}
}

function getFromAddress(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 from = inputAddresses[0]
return inputAddresses.slice(1).every((addr) => addr === from) ? from : undefined
} catch (_) {
return undefined
}
}

function checkOutputAddress(tx: Transaction, from: Address, to: Address): boolean {
let fromCount = 0
let toCount = 0
tx.unsigned.fixedOutputs.forEach((o) => {
if (o.address === from) {
fromCount += 1
} else if (o.address === to) {
toCount += 1
}
})
const outputCount = tx.unsigned.fixedOutputs.length
return toCount === 1 && fromCount === outputCount - 1
}

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 output = tx.unsigned.fixedOutputs.find((o) => o.address === to)!
return output.attoAlphAmount === DUST_AMOUNT.toString() && output.tokens.length === 1
}

function isDepositTransaction(tx: Transaction, exchangeAddress: string): boolean {
if (
tx.contractInputs.length !== 0 ||
tx.generatedOutputs.length !== 0 ||
tx.unsigned.inputs.length === 0 ||
tx.unsigned.scriptOpt !== undefined
) {
return false
}
const from = getFromAddress(tx)
if (from === undefined) {
return false
}
return checkOutputAddress(tx, from, exchangeAddress)
}
1 change: 1 addition & 0 deletions packages/web3/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ export * from './utils'
export * from './subscription'
export * from './sign'
export * from './number'
export { isExchangeAddress, isDepositALPHTransaction, isDepositTokenTransaction, getDepositAddress } from './exchange'
Loading
Loading