Skip to content

Commit d962ea8

Browse files
committed
feat: update bridge memoHex encoding/decoding and remove upgrade date config
1 parent c48e1ba commit d962ea8

File tree

8 files changed

+170
-137
lines changed

8 files changed

+170
-137
lines changed

ironfish-cli/src/commands/wallet/chainport/send.ts

Lines changed: 19 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,14 @@ import { IronfishCommand } from '../../../command'
1919
import { HexFlag, IronFlag, RemoteFlags, ValueFlag } from '../../../flags'
2020
import * as ui from '../../../ui'
2121
import {
22-
ChainportBridgeFeeV2,
2322
ChainportBridgeTransaction,
2423
ChainportToken,
2524
ChainportTokenWithNetwork,
2625
fetchChainportBridgeTransaction,
2726
fetchChainportTokenPaths,
2827
fetchChainportTokens,
29-
isBridgeFeeV1,
30-
isBridgeFeeV2,
3128
} from '../../../utils/chainport'
3229
import { isEthereumAddress } from '../../../utils/chainport/address'
33-
import { getConfig } from '../../../utils/chainport/config'
3430
import { promptCurrency } from '../../../utils/currency'
3531
import { promptExpiration } from '../../../utils/expiration'
3632
import { getExplorer } from '../../../utils/explorer'
@@ -353,30 +349,17 @@ export class BridgeCommand extends IronfishCommand {
353349
}
354350

355351
const outputs = [userOutput, gasOutput]
356-
const isChainportFeeUpgradeActive = new Date() > getConfig(networkId).bridgeFeeUpgrade
357-
const bridgeFeeV1 = isBridgeFeeV1(txn.bridge_fee)
358-
const bridgeFeeV2 = isBridgeFeeV2(txn.bridge_fee)
359-
if (isChainportFeeUpgradeActive) {
360-
if (!bridgeFeeV2) {
361-
throw new Error('Bridge fee upgrade is active but the bridge fee is not v2')
362-
} else {
363-
const bridgeFee = txn.bridge_fee as ChainportBridgeFeeV2
364-
const bridgeFeeAmount = BigInt(bridgeFee.source_token_fee_amount)
365-
if (bridgeFeeAmount > 0n) {
366-
userOutput.amount = (BigInt(userOutput.amount) - bridgeFeeAmount).toString()
367-
const bridgeFeeOutput = {
368-
publicAddress: bridgeFee.publicAddress,
369-
amount: bridgeFeeAmount.toString(),
370-
memo: bridgeFee.memo,
371-
assetId: bridgeFee.assetId,
372-
}
373-
outputs.push(bridgeFeeOutput)
374-
}
375-
}
376-
} else {
377-
if (!bridgeFeeV1) {
378-
throw new Error('Bridge fee upgrade is not active but the bridge fee is not v1')
352+
353+
const bridgeFeeAmount = BigInt(txn.bridge_fee.source_token_fee_amount)
354+
if (bridgeFeeAmount > 0n) {
355+
userOutput.amount = (BigInt(userOutput.amount) - bridgeFeeAmount).toString()
356+
const bridgeFeeOutput = {
357+
publicAddress: txn.bridge_fee.publicAddress,
358+
amount: bridgeFeeAmount.toString(),
359+
memo: txn.bridge_fee.memo,
360+
assetId: txn.bridge_fee.assetId,
379361
}
362+
outputs.push(bridgeFeeOutput)
380363
}
381364

382365
const params: CreateTransactionRequest = {
@@ -442,31 +425,15 @@ export class BridgeCommand extends IronfishCommand {
442425

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

445-
let chainportFee: string
446-
447-
if ('is_portx_fee_payment' in txn.bridge_fee && txn.bridge_fee.is_portx_fee_payment) {
448-
this.logger.log('\nStaked PortX detected')
449-
450-
chainportFee = CurrencyUtils.render(
451-
BigInt(txn.bridge_fee.portx_fee_amount),
452-
true,
453-
'portx asset id',
454-
{
455-
decimals: 18,
456-
symbol: 'PORTX',
457-
},
458-
)
459-
} else {
460-
chainportFee = CurrencyUtils.render(
461-
BigInt(txn.bridge_fee.source_token_fee_amount ?? 0),
462-
true,
463-
'chainport fee id',
464-
{
465-
decimals: sourceToken.decimals,
466-
symbol: sourceAsset.verification.symbol,
467-
},
468-
)
469-
}
428+
const chainportFee = CurrencyUtils.render(
429+
BigInt(txn.bridge_fee.source_token_fee_amount ?? 0),
430+
true,
431+
'chainport fee id',
432+
{
433+
decimals: sourceToken.decimals,
434+
symbol: sourceAsset.verification.symbol,
435+
},
436+
)
470437

471438
const summary = `\
472439
\nBRIDGE TRANSACTION SUMMARY:

ironfish-cli/src/utils/chainport/config.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ const config = {
1515
incomingAddresses: new Set([
1616
'06102d319ab7e77b914a1bd135577f3e266fd82a3e537a02db281421ed8b3d13',
1717
]),
18-
bridgeFeeUpgrade: new Date('2025-07-01T00:00:00Z'),
1918
},
2019
[MAINNET.id]: {
2120
endpoint: 'https://api.ironfish.network/',
@@ -26,7 +25,6 @@ const config = {
2625
incomingAddresses: new Set([
2726
'1216302193e8f1ad020f458b54a163039403d803e98673c6a85e59b5f4a1a900',
2827
]),
29-
bridgeFeeUpgrade: new Date('2025-07-15T00:00:00Z'),
3028
},
3129
}
3230

ironfish-cli/src/utils/chainport/metadata.test.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,31 +38,50 @@ describe('ChainportMemoMetadata', () => {
3838

3939
test('decode decodes encoded hex string correctly', () => {
4040
expect(
41-
ChainportMemoMetadata.decodeV1(
41+
ChainportMemoMetadata.decode(
4242
'000214d3c11c03c10481c50cc28e24228a30220620a08c08530c2c614220f24a',
4343
),
4444
).toEqual([2, '0x5DF170F118753CaE92aaC2868A2C25Ccb6528f9a'.toLowerCase(), false])
4545

4646
expect(
47-
ChainportMemoMetadata.decodeV1(
47+
ChainportMemoMetadata.decode(
4848
'02161ca1882c130f04f04638f2092851863c518018c0012ca1093c438b10200a',
4949
),
5050
).toEqual([22, '0x7A68B1Cf1F16Ef89A566F5606C01BA49F4Eb420A'.toLowerCase(), true])
5151

5252
expect(
53-
ChainportMemoMetadata.decodeV2(
54-
'01742d35cc6634c0532925a3b8d4c9db96c4b4d8b668775c9100000000000000',
53+
ChainportMemoMetadata.decode(
54+
'004f99a1a130db7faf2d00d729ad1fc41c76547c5646d10f28e0000000000000',
5555
),
56-
).toEqual([1, '0x742d35cc6634c0532925a3b8d4c9db96c4b4d8b6'.toLowerCase(), false])
56+
).toEqual([15, '0x99A1a130DB7FAf2d00d729aD1FC41c76547c5646'.toLowerCase(), false])
5757
})
5858

59-
test('encode and decode are reversible', () => {
59+
test('encode and decode are reversible v1', () => {
6060
const networkId = 2
6161
const address = '5DF170F118753CaE92aaC2868A2C25Ccb6528f9a'
6262
const toIronfish = false
6363

6464
const encoded = ChainportMemoMetadata.encode(networkId, address, toIronfish)
65-
const decoded = ChainportMemoMetadata.decodeV1(encoded)
65+
const decoded = ChainportMemoMetadata.decode(encoded)
66+
67+
expect(decoded).toEqual([networkId, '0x' + address.toLowerCase(), toIronfish])
68+
})
69+
70+
test('encode and decode are reversible v2', () => {
71+
const networkId = 2
72+
const address = '5DF170F118753CaE92aaC2868A2C25Ccb6528f9a'
73+
const toIronfish = false
74+
const timestamp = 1753715824
75+
const version = 1
76+
77+
const encoded = ChainportMemoMetadata.encodeV2(
78+
networkId,
79+
address,
80+
toIronfish,
81+
timestamp,
82+
version,
83+
)
84+
const decoded = ChainportMemoMetadata.decode(encoded)
6685

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

ironfish-cli/src/utils/chainport/metadata.ts

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ export class ChainportMemoMetadata {
4444
return String.fromCharCode(num - 10 + 'a'.charCodeAt(0))
4545
}
4646

47+
public static convertHexToBinary(encodedHex: string): string {
48+
const buffer = Buffer.from(encodedHex, 'hex')
49+
let binaryString = ''
50+
for (let i = 0; i < buffer.length; i++) {
51+
binaryString += buffer[i].toString(2).padStart(8, '0')
52+
}
53+
return binaryString
54+
}
55+
4756
public static encode(networkId: number, address: string, toIronfish: boolean) {
4857
if (address.startsWith('0x')) {
4958
address = address.slice(2)
@@ -63,6 +72,89 @@ export class ChainportMemoMetadata {
6372
return hexString.padStart(64, '0')
6473
}
6574

75+
public static encodeV2(
76+
networkId: number,
77+
address: string,
78+
toIronfish: boolean,
79+
timestamp: number,
80+
version: number,
81+
) {
82+
if (networkId >= 1 << 6) {
83+
throw new Error('networkId exceeds 6-bit capacity')
84+
}
85+
if (version >= 1 << 2) {
86+
throw new Error('version exceeds 2-bit capacity')
87+
}
88+
if (BigInt(timestamp) >= 1n << 31n) {
89+
throw new Error('timestamp exceeds 31-bit capacity')
90+
}
91+
92+
let addressClean = address
93+
if (addressClean.startsWith('0x')) {
94+
addressClean = addressClean.slice(2)
95+
}
96+
97+
if (addressClean.length !== 40) {
98+
throw new Error('address must be 40 hexadecimal characters')
99+
}
100+
101+
const addrBytes = Buffer.from(addressClean, 'hex')
102+
103+
if (addrBytes.length !== 20) {
104+
throw new Error('address must decode to 20 bytes')
105+
}
106+
107+
const bitArray: number[] = new Array(256).fill(0) as number[]
108+
let pos = 0
109+
110+
pos += 6
111+
112+
bitArray[pos] = toIronfish ? 1 : 0
113+
pos += 1
114+
115+
pos += 1
116+
117+
bitArray[pos] = (version >> 1) & 1
118+
bitArray[pos + 1] = version & 1
119+
pos += 2
120+
121+
for (let i = 0; i < 6; i++) {
122+
bitArray[pos + i] = (networkId >> (5 - i)) & 1
123+
}
124+
pos += 6
125+
126+
for (const byte of addrBytes) {
127+
for (let i = 0; i < 8; i++) {
128+
bitArray[pos] = (byte >> (7 - i)) & 1
129+
pos += 1
130+
}
131+
}
132+
133+
for (let i = 0; i < 31; i++) {
134+
bitArray[pos + i] = (timestamp >> (30 - i)) & 1
135+
}
136+
pos += 31
137+
138+
pos += 49
139+
140+
if (pos !== 256) {
141+
throw new Error('pos is not 256')
142+
}
143+
144+
const result = new Uint8Array(32)
145+
for (let i = 0; i < 32; i++) {
146+
let byte = 0
147+
for (let j = 0; j < 8; j++) {
148+
byte = (byte << 1) | bitArray[i * 8 + j]
149+
}
150+
result[i] = byte
151+
}
152+
153+
return Array.from(result)
154+
.map((byte) => byte.toString(16).padStart(2, '0'))
155+
.join('')
156+
}
157+
66158
public static decodeV1(encodedHex: string): [number, string, boolean] {
67159
const hexInteger = BigInt('0x' + encodedHex)
68160
const encodedString = hexInteger.toString(2)
@@ -84,12 +176,30 @@ export class ChainportMemoMetadata {
84176
}
85177

86178
public static decodeV2(encodedHex: string): [number, string, boolean] {
87-
const bytes = Buffer.from(encodedHex, 'hex')
88-
const networkId = bytes.readUInt8(0)
89-
const addressBytes = bytes.subarray(1, 21)
90-
const address = '0x' + addressBytes.toString('hex')
91-
const toIronfish = (bytes[21] & 0x80) !== 0
179+
const bits = this.convertHexToBinary(encodedHex)
180+
const toIronfish = bits[6] === '1'
181+
const memoHexVersion = bits.slice(8, 10)
182+
if (memoHexVersion !== '01') {
183+
throw new Error(`Unexpected memoHex version: ${memoHexVersion}`)
184+
}
185+
186+
const networkIdBits = bits.slice(10, 16)
187+
const networkId = parseInt(networkIdBits, 2)
188+
const addressBits = bits.slice(16, 176)
189+
let address = '0x'
190+
for (let i = 0; i < addressBits.length; i += 4) {
191+
address += parseInt(addressBits.slice(i, i + 4), 2).toString(16)
192+
}
92193

93194
return [networkId, address.toLowerCase(), toIronfish]
94195
}
196+
197+
public static decode(encodedHex: string): [number, string, boolean] {
198+
const bits = this.convertHexToBinary(encodedHex)
199+
const memoHexVersion = bits.slice(8, 10)
200+
if (memoHexVersion === '01') {
201+
return this.decodeV2(encodedHex)
202+
}
203+
return this.decodeV1(encodedHex)
204+
}
95205
}

ironfish-cli/src/utils/chainport/requests.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,7 @@ export const fetchChainportBridgeTransaction = async (
6565
url.searchParams.append('asset_id', assetId)
6666
url.searchParams.append('target_network_id', targetNetworkId.toString())
6767
url.searchParams.append('target_address', targetAddress.toString())
68-
const isBridgeFeeUpgradeActivated = new Date(config.bridgeFeeUpgrade) < new Date()
69-
70-
if (isBridgeFeeUpgradeActivated) {
71-
url.searchParams.append('source_address', sourceAddress)
72-
}
68+
url.searchParams.append('source_address', sourceAddress)
7369

7470
return await makeChainportRequest<ChainportBridgeTransaction>(url.toString())
7571
}

ironfish-cli/src/utils/chainport/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export type ChainportBridgeTransaction = {
2929
amount: string
3030
memo: string
3131
}
32-
bridge_fee: ChainportBridgeFeeV1 | ChainportBridgeFeeV2
32+
bridge_fee: ChainportBridgeFeeV2
3333
}
3434

3535
export type ChainportNetwork = {

ironfish-cli/src/utils/chainport/utils.test.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ describe('isChainportTransaction', () => {
1414
const mockConfig = {
1515
incomingAddresses: new Set(['incoming1', 'incoming2']),
1616
outgoingAddresses: new Set(['outgoing1', 'outgoing2']),
17-
bridgeFeeUpgrade: new Date('2025-07-01T00:00:00Z'),
1817
}
1918

2019
beforeEach(() => {
@@ -44,11 +43,10 @@ describe('isChainportTransaction', () => {
4443
})
4544

4645
it('should return true for valid incoming chainport transaction', () => {
47-
;(ChainportMemoMetadata.decodeV1 as Mock).mockReturnValue([1, 'address', true])
46+
;(ChainportMemoMetadata.decode as Mock).mockReturnValue([1, 'address'])
4847

4948
const transaction = {
5049
type: TransactionType.RECEIVE,
51-
timestamp: new Date('2024-01-01T00:00:00Z').getTime(),
5250
notes: [{ sender: 'incoming1', memoHex: 'mockHex' }] as RpcWalletNote[],
5351
} as RpcWalletTransaction
5452
const result = extractChainportDataFromTransaction(1, transaction)
@@ -95,10 +93,9 @@ describe('isChainportTransaction', () => {
9593
})
9694

9795
it('should return true for valid outgoing chainport transaction with bridge fee v1', () => {
98-
;(ChainportMemoMetadata.decodeV1 as Mock).mockReturnValue([1, 'address', false])
96+
;(ChainportMemoMetadata.decode as Mock).mockReturnValue([1, 'address'])
9997
const transaction = {
10098
type: TransactionType.SEND,
101-
timestamp: new Date('2024-01-01T00:00:00Z').getTime(),
10299
notes: [
103100
{ owner: 'outgoing1', memo: '{"type": "fee_payment"}', memoHex: 'mockHex' },
104101
{ owner: 'outgoing1', memo: '', memoHex: 'mockHex' },
@@ -114,10 +111,9 @@ describe('isChainportTransaction', () => {
114111
})
115112

116113
it('should return true for valid outgoing chainport transaction with bridge fee v2', () => {
117-
;(ChainportMemoMetadata.decodeV2 as Mock).mockReturnValue([1, 'address', false])
114+
;(ChainportMemoMetadata.decode as Mock).mockReturnValue([1, 'address'])
118115
const transaction = {
119116
type: TransactionType.SEND,
120-
timestamp: new Date('2026-01-01T00:00:00Z').getTime(),
121117
notes: [
122118
{ owner: 'outgoing1', memo: '{"type": "fee_payment"}', memoHex: 'mockHex' },
123119
{ owner: 'outgoing1', memo: '', memoHex: 'mockHex' },

0 commit comments

Comments
 (0)