Skip to content
Open
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
79 changes: 41 additions & 38 deletions ironfish-cli/src/commands/wallet/chainport/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,10 @@ export class BridgeCommand extends IronfishCommand {
expiration: number | undefined,
) {
const { flags } = await this.parse(BridgeCommand)
const response = await client.wallet.getAccountPublicKey({
account: from,
})
const sourceAddress = response.content.publicKey

ux.action.start('Fetching bridge transaction details')
const txn = await fetchChainportBridgeTransaction(
Expand All @@ -328,24 +332,39 @@ export class BridgeCommand extends IronfishCommand {
asset.web3_address,
tokenWithNetwork.network.chainport_network_id,
to,
sourceAddress,
)
ux.action.stop()

const userOutput = {
publicAddress: txn.bridge_output.publicAddress,
amount: txn.bridge_output.amount,
memoHex: txn.bridge_output.memoHex,
assetId: txn.bridge_output.assetId,
}
const gasOutput = {
publicAddress: txn.gas_fee_output.publicAddress,
amount: txn.gas_fee_output.amount,
memo: txn.gas_fee_output.memo,
}

const outputs = [userOutput, gasOutput]

const bridgeFeeAmount = BigInt(txn.bridge_fee.source_token_fee_amount)
if (bridgeFeeAmount > 0n) {
userOutput.amount = (BigInt(userOutput.amount) - bridgeFeeAmount).toString()
const bridgeFeeOutput = {
publicAddress: txn.bridge_fee.publicAddress,
amount: bridgeFeeAmount.toString(),
memo: txn.bridge_fee.memo,
assetId: txn.bridge_fee.assetId,
}
outputs.push(bridgeFeeOutput)
}

const params: CreateTransactionRequest = {
account: from,
outputs: [
{
publicAddress: txn.bridge_output.publicAddress,
amount: txn.bridge_output.amount,
memoHex: txn.bridge_output.memoHex,
assetId: txn.bridge_output.assetId,
},
{
publicAddress: txn.gas_fee_output.publicAddress,
amount: txn.gas_fee_output.amount,
memo: txn.gas_fee_output.memo,
},
],
outputs,
fee: flags.fee ? CurrencyUtils.encode(flags.fee) : null,
feeRate: flags.feeRate ? CurrencyUtils.encode(flags.feeRate) : null,
expiration,
Expand Down Expand Up @@ -406,31 +425,15 @@ export class BridgeCommand extends IronfishCommand {

const targetNetworkFee = CurrencyUtils.render(BigInt(txn.gas_fee_output.amount), true)

let chainportFee: string

if (txn.bridge_fee.is_portx_fee_payment) {
this.logger.log('\nStaked PortX detected')

chainportFee = CurrencyUtils.render(
BigInt(txn.bridge_fee.portx_fee_amount),
true,
'portx asset id',
{
decimals: 18,
symbol: 'PORTX',
},
)
} else {
chainportFee = CurrencyUtils.render(
BigInt(txn.bridge_fee.source_token_fee_amount ?? 0),
true,
'chainport fee id',
{
decimals: sourceToken.decimals,
symbol: sourceAsset.verification.symbol,
},
)
}
const chainportFee = CurrencyUtils.render(
BigInt(txn.bridge_fee.source_token_fee_amount ?? 0),
true,
'chainport fee id',
{
decimals: sourceToken.decimals,
symbol: sourceAsset.verification.symbol,
},
)

const summary = `\
\nBRIDGE TRANSACTION SUMMARY:
Expand Down
95 changes: 94 additions & 1 deletion ironfish-cli/src/utils/chainport/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,15 @@ describe('ChainportMemoMetadata', () => {
'02161ca1882c130f04f04638f2092851863c518018c0012ca1093c438b10200a',
),
).toEqual([22, '0x7A68B1Cf1F16Ef89A566F5606C01BA49F4Eb420A'.toLowerCase(), true])

expect(
ChainportMemoMetadata.decode(
'004f99a1a130db7faf2d00d729ad1fc41c76547c5646d10f28e0000000000000',
),
).toEqual([15, '0x99A1a130DB7FAf2d00d729aD1FC41c76547c5646'.toLowerCase(), false])
})

test('encode and decode are reversible', () => {
test('encode and decode are reversible v1', () => {
const networkId = 2
const address = '5DF170F118753CaE92aaC2868A2C25Ccb6528f9a'
const toIronfish = false
Expand All @@ -60,4 +66,91 @@ describe('ChainportMemoMetadata', () => {

expect(decoded).toEqual([networkId, '0x' + address.toLowerCase(), toIronfish])
})

test('encode and decode are reversible v2', () => {
const networkId = 2
const address = '5DF170F118753CaE92aaC2868A2C25Ccb6528f9a'
const toIronfish = false
const timestamp = 1753715824
const version = 1

const encoded = ChainportMemoMetadata.encodeV2(
networkId,
address,
toIronfish,
timestamp,
version,
)
const decoded = ChainportMemoMetadata.decode(encoded)

expect(decoded).toEqual([networkId, '0x' + address.toLowerCase(), toIronfish])
})

test('should throw error if networkId is greater than 63', () => {
const networkId = 64
const address = '5DF170F118753CaE92aaC2868A2C25Ccb6528f9a'
const toIronfish = false
const timestamp = 1753715824
const version = 1

expect(() =>
ChainportMemoMetadata.encodeV2(networkId, address, toIronfish, timestamp, version),
).toThrow('networkId exceeds 6-bit capacity')
})

test('should throw error if version is greater than 3', () => {
const networkId = 2
const address = '5DF170F118753CaE92aaC2868A2C25Ccb6528f9a'
const toIronfish = false
const timestamp = 1753715824
const version = 4

expect(() =>
ChainportMemoMetadata.encodeV2(networkId, address, toIronfish, timestamp, version),
).toThrow('version exceeds 2-bit capacity')
})

test('should throw error if timestamp is greater than 2147483647', () => {
const networkId = 2
const address = '5DF170F118753CaE92aaC2868A2C25Ccb6528f9a'
const toIronfish = false
const timestamp = 2147483648
const version = 1

expect(() =>
ChainportMemoMetadata.encodeV2(networkId, address, toIronfish, timestamp, version),
).toThrow('timestamp exceeds 31-bit capacity')
})

test('should throw error if address is not 40 hexadecimal characters', () => {
const networkId = 2
const address = '5DF170F118753CaE92aaC2868A2C25Ccb6528f9atest'
const toIronfish = false
const timestamp = 1753715824
const version = 1

expect(() =>
ChainportMemoMetadata.encodeV2(networkId, address, toIronfish, timestamp, version),
).toThrow('address must be 40 hexadecimal characters')
})

test('should throw error if memoHex version is not 1 for decodeV2', () => {
const networkId = 2
const address = '5DF170F118753CaE92aaC2868A2C25Ccb6528f9a'
const toIronfish = false
const timestamp = 1753715824
const version = 2

const encoded = ChainportMemoMetadata.encodeV2(
networkId,
address,
toIronfish,
timestamp,
version,
)

expect(() => ChainportMemoMetadata.decodeV2(encoded)).toThrow(
'Unexpected memoHex version: 10',
)
})
})
118 changes: 117 additions & 1 deletion ironfish-cli/src/utils/chainport/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ export class ChainportMemoMetadata {
return String.fromCharCode(num - 10 + 'a'.charCodeAt(0))
}

public static convertHexToBinary(encodedHex: string): string {
const buffer = Buffer.from(encodedHex, 'hex')
let binaryString = ''
for (let i = 0; i < buffer.length; i++) {
binaryString += buffer[i].toString(2).padStart(8, '0')
}
return binaryString
}

public static encode(networkId: number, address: string, toIronfish: boolean) {
if (address.startsWith('0x')) {
address = address.slice(2)
Expand All @@ -63,7 +72,86 @@ export class ChainportMemoMetadata {
return hexString.padStart(64, '0')
}

public static decode(encodedHex: string): [number, string, boolean] {
public static encodeV2(
networkId: number,
address: string,
toIronfish: boolean,
timestamp: number,
version: number,
) {
if (networkId >= 1 << 6) {
throw new Error('networkId exceeds 6-bit capacity')
}
if (version >= 1 << 2) {
throw new Error('version exceeds 2-bit capacity')
}
if (BigInt(timestamp) >= 1n << 31n) {
throw new Error('timestamp exceeds 31-bit capacity')
}

let addressClean = address
if (addressClean.startsWith('0x')) {
addressClean = addressClean.slice(2)
}

if (addressClean.length !== 40) {
throw new Error('address must be 40 hexadecimal characters')
}

const addrBytes = Buffer.from(addressClean, 'hex')

if (addrBytes.length !== 20) {
throw new Error('address must decode to 20 bytes')
}

const bitArray: number[] = new Array(256).fill(0) as number[]
let pos = 0

pos += 6

bitArray[pos] = toIronfish ? 1 : 0
pos += 1

pos += 1

bitArray[pos] = (version >> 1) & 1
bitArray[pos + 1] = version & 1
pos += 2

for (let i = 0; i < 6; i++) {
bitArray[pos + i] = (networkId >> (5 - i)) & 1
}
pos += 6

for (const byte of addrBytes) {
for (let i = 0; i < 8; i++) {
bitArray[pos] = (byte >> (7 - i)) & 1
pos += 1
}
}

for (let i = 0; i < 31; i++) {
bitArray[pos + i] = (timestamp >> (30 - i)) & 1
}
pos += 31

pos += 49

const result = new Uint8Array(32)
for (let i = 0; i < 32; i++) {
let byte = 0
for (let j = 0; j < 8; j++) {
byte = (byte << 1) | bitArray[i * 8 + j]
}
result[i] = byte
}

return Array.from(result)
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('')
}

public static decodeV1(encodedHex: string): [number, string, boolean] {
const hexInteger = BigInt('0x' + encodedHex)
const encodedString = hexInteger.toString(2)
const padded = encodedString.padStart(250, '0')
Expand All @@ -82,4 +170,32 @@ export class ChainportMemoMetadata {

return [networkId, address.toLowerCase(), toIronfish]
}

public static decodeV2(encodedHex: string): [number, string, boolean] {
const bits = this.convertHexToBinary(encodedHex)
const toIronfish = bits[6] === '1'
const memoHexVersion = bits.slice(8, 10)
if (memoHexVersion !== '01') {
throw new Error(`Unexpected memoHex version: ${memoHexVersion}`)
}

const networkIdBits = bits.slice(10, 16)
const networkId = parseInt(networkIdBits, 2)
const addressBits = bits.slice(16, 176)
let address = '0x'
for (let i = 0; i < addressBits.length; i += 4) {
address += parseInt(addressBits.slice(i, i + 4), 2).toString(16)
}

return [networkId, address.toLowerCase(), toIronfish]
}

public static decode(encodedHex: string): [number, string, boolean] {
const bits = this.convertHexToBinary(encodedHex)
const memoHexVersion = bits.slice(8, 10)
if (memoHexVersion === '01') {
return this.decodeV2(encodedHex)
}
return this.decodeV1(encodedHex)
}
}
2 changes: 2 additions & 0 deletions ironfish-cli/src/utils/chainport/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,15 @@ export const fetchChainportBridgeTransaction = async (
assetId: string,
targetNetworkId: number,
targetAddress: string,
sourceAddress: string,
): Promise<ChainportBridgeTransaction> => {
const config = getConfig(networkId)
const url = new URL(`/bridges/transactions/create`, config.endpoint)
url.searchParams.append('amount', amount.toString())
url.searchParams.append('asset_id', assetId)
url.searchParams.append('target_network_id', targetNetworkId.toString())
url.searchParams.append('target_address', targetAddress.toString())
url.searchParams.append('source_address', sourceAddress)

return await makeChainportRequest<ChainportBridgeTransaction>(url.toString())
}
Expand Down
19 changes: 14 additions & 5 deletions ironfish-cli/src/utils/chainport/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@

// This file contains response types for chainport requests

export type ChainportBridgeFeeV1 = {
source_token_fee_amount: string
portx_fee_amount: string
is_portx_fee_payment: boolean
}

export type ChainportBridgeFeeV2 = {
publicAddress: string
source_token_fee_amount: string
memo: string
assetId: string
}

export type ChainportBridgeTransaction = {
bridge_output: {
publicAddress: string
Expand All @@ -16,11 +29,7 @@ export type ChainportBridgeTransaction = {
amount: string
memo: string
}
bridge_fee: {
source_token_fee_amount: string
portx_fee_amount: string
is_portx_fee_payment: boolean
}
bridge_fee: ChainportBridgeFeeV2
}

export type ChainportNetwork = {
Expand Down
Loading
Loading