@xyo-network/chain-bridge 1.15.2 → 1.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +1 -1
  2. package/dist/node/driver/indexer/ChainHydratedBlocksObservable.d.ts +4 -4
  3. package/dist/node/driver/indexer/ChainHydratedBlocksObservable.d.ts.map +1 -1
  4. package/dist/node/driver/mongo/MongoMap.d.ts +3 -2
  5. package/dist/node/driver/mongo/MongoMap.d.ts.map +1 -1
  6. package/dist/node/index.mjs +300 -683
  7. package/dist/node/index.mjs.map +1 -1
  8. package/dist/node/interface/interface/IntentIndexerInterface.d.ts +5 -3
  9. package/dist/node/interface/interface/IntentIndexerInterface.d.ts.map +1 -1
  10. package/dist/node/interface/service/Observer/ERC20TransferObserver/ERC20TransferObserver.d.ts +28 -0
  11. package/dist/node/interface/service/Observer/ERC20TransferObserver/ERC20TransferObserver.d.ts.map +1 -0
  12. package/dist/node/interface/service/Observer/ERC20TransferObserver/index.d.ts +2 -0
  13. package/dist/node/interface/service/Observer/ERC20TransferObserver/index.d.ts.map +1 -0
  14. package/dist/node/interface/service/Observer/ERC20TransferObserver/spec/ERC20TransferObserver.spec.d.ts +2 -0
  15. package/dist/node/interface/service/Observer/ERC20TransferObserver/spec/ERC20TransferObserver.spec.d.ts.map +1 -0
  16. package/dist/node/interface/service/Observer/LiquidityPoolBridgeObserver/LiquidityPoolBridgeObserver.d.ts +36 -0
  17. package/dist/node/interface/service/Observer/LiquidityPoolBridgeObserver/LiquidityPoolBridgeObserver.d.ts.map +1 -0
  18. package/dist/node/interface/service/Observer/LiquidityPoolBridgeObserver/index.d.ts +2 -0
  19. package/dist/node/interface/service/Observer/LiquidityPoolBridgeObserver/index.d.ts.map +1 -0
  20. package/dist/node/interface/service/Observer/LiquidityPoolBridgeObserver/spec/LiquidityPoolBridgeObserver.spec.d.ts +2 -0
  21. package/dist/node/interface/service/Observer/LiquidityPoolBridgeObserver/spec/LiquidityPoolBridgeObserver.spec.d.ts.map +1 -0
  22. package/dist/node/interface/service/Observer/Observer.d.ts +1 -1
  23. package/dist/node/interface/service/Observer/Observer.d.ts.map +1 -1
  24. package/dist/node/interface/service/{ChainBridgeRelay → Relay/ChainBridgeRelay}/ChainBridgeRelayInterface.d.ts +1 -1
  25. package/dist/node/interface/service/Relay/ChainBridgeRelay/ChainBridgeRelayInterface.d.ts.map +1 -0
  26. package/dist/node/interface/service/{ChainBridgeRelay → Relay/ChainBridgeRelay}/ChainBridgeRelayService.d.ts +1 -1
  27. package/dist/node/interface/service/Relay/ChainBridgeRelay/ChainBridgeRelayService.d.ts.map +1 -0
  28. package/dist/node/interface/service/Relay/ChainBridgeRelay/index.d.ts.map +1 -0
  29. package/dist/node/interface/service/Relay/ChainBridgeRelay/spec/ChainBridgeRelayService.spec.d.ts.map +1 -0
  30. package/dist/node/interface/service/Relay/LiquidityPoolBridgeRelay/LiquidityPoolBridgeRelay.d.ts +57 -0
  31. package/dist/node/interface/service/Relay/LiquidityPoolBridgeRelay/LiquidityPoolBridgeRelay.d.ts.map +1 -0
  32. package/dist/node/interface/service/Relay/LiquidityPoolBridgeRelay/index.d.ts +2 -0
  33. package/dist/node/interface/service/Relay/LiquidityPoolBridgeRelay/index.d.ts.map +1 -0
  34. package/dist/node/interface/service/Relay/LiquidityPoolBridgeRelay/spec/LiquidityPoolBridgeRelay.spec.d.ts +2 -0
  35. package/dist/node/interface/service/Relay/LiquidityPoolBridgeRelay/spec/LiquidityPoolBridgeRelay.spec.d.ts.map +1 -0
  36. package/dist/node/interface/service/Relay/index.d.ts +2 -0
  37. package/dist/node/interface/service/Relay/index.d.ts.map +1 -0
  38. package/dist/node/interface/service/index.d.ts +1 -1
  39. package/dist/node/interface/service/index.d.ts.map +1 -1
  40. package/dist/node/manifest/getLocator.d.ts.map +1 -1
  41. package/dist/node/manifest/public/index.d.ts +6 -2
  42. package/dist/node/manifest/public/index.d.ts.map +1 -1
  43. package/dist/node/server/app.d.ts +1 -2
  44. package/dist/node/server/app.d.ts.map +1 -1
  45. package/dist/node/server/routes/addRoutes.d.ts.map +1 -1
  46. package/dist/node/server/routes/bridge/addBridgeRoutes.d.ts +3 -0
  47. package/dist/node/server/routes/bridge/addBridgeRoutes.d.ts.map +1 -0
  48. package/dist/node/server/routes/bridge/index.d.ts +2 -0
  49. package/dist/node/server/routes/bridge/index.d.ts.map +1 -0
  50. package/dist/node/server/routes/bridge/middleware/index.d.ts +2 -0
  51. package/dist/node/server/routes/bridge/middleware/index.d.ts.map +1 -0
  52. package/dist/node/server/routes/bridge/middleware/requestHandlerValidator.d.ts +32 -0
  53. package/dist/node/server/routes/bridge/middleware/requestHandlerValidator.d.ts.map +1 -0
  54. package/dist/node/server/routes/bridge/routeDefinitions/getRouteDefinitions.d.ts +3 -0
  55. package/dist/node/server/routes/bridge/routeDefinitions/getRouteDefinitions.d.ts.map +1 -0
  56. package/dist/node/server/routes/bridge/routeDefinitions/index.d.ts +2 -0
  57. package/dist/node/server/routes/bridge/routeDefinitions/index.d.ts.map +1 -0
  58. package/dist/node/server/routes/bridge/routeDefinitions/pathParams/ChainIdPathParam.d.ts +6 -0
  59. package/dist/node/server/routes/bridge/routeDefinitions/pathParams/ChainIdPathParam.d.ts.map +1 -0
  60. package/dist/node/server/routes/bridge/routeDefinitions/pathParams/index.d.ts +2 -0
  61. package/dist/node/server/routes/bridge/routeDefinitions/pathParams/index.d.ts.map +1 -0
  62. package/dist/node/server/routes/bridge/routeDefinitions/routeDefinition.d.ts +8 -0
  63. package/dist/node/server/routes/bridge/routeDefinitions/routeDefinition.d.ts.map +1 -0
  64. package/dist/node/server/routes/bridge/routeDefinitions/routes/bridgeFromRemoteStatus.d.ts +3 -0
  65. package/dist/node/server/routes/bridge/routeDefinitions/routes/bridgeFromRemoteStatus.d.ts.map +1 -0
  66. package/dist/node/server/routes/bridge/routeDefinitions/routes/bridgeToRemote.d.ts +3 -0
  67. package/dist/node/server/routes/bridge/routeDefinitions/routes/bridgeToRemote.d.ts.map +1 -0
  68. package/dist/node/server/routes/bridge/routeDefinitions/routes/bridgeToRemoteEstimate.d.ts +3 -0
  69. package/dist/node/server/routes/bridge/routeDefinitions/routes/bridgeToRemoteEstimate.d.ts.map +1 -0
  70. package/dist/node/server/routes/bridge/routeDefinitions/routes/bridgeToRemoteStatus.d.ts +3 -0
  71. package/dist/node/server/routes/bridge/routeDefinitions/routes/bridgeToRemoteStatus.d.ts.map +1 -0
  72. package/dist/node/server/routes/bridge/routeDefinitions/routes/index.d.ts +5 -0
  73. package/dist/node/server/routes/bridge/routeDefinitions/routes/index.d.ts.map +1 -0
  74. package/dist/node/server/routes/healthz/get.d.ts +2 -1
  75. package/dist/node/server/routes/healthz/get.d.ts.map +1 -1
  76. package/dist/node/server/routes/index.d.ts +0 -1
  77. package/dist/node/server/routes/index.d.ts.map +1 -1
  78. package/dist/node/server/server.d.ts.map +1 -1
  79. package/package.json +62 -55
  80. package/src/driver/indexer/ChainHydratedBlocksObservable.ts +5 -5
  81. package/src/driver/indexer/spec/ChainBlocksObservable.spec.ts +6 -3
  82. package/src/driver/indexer/spec/ChainHydratedBlocksObservable.spec.ts +10 -4
  83. package/src/driver/mongo/MongoMap.ts +13 -3
  84. package/src/interface/interface/IntentIndexerInterface.ts +5 -4
  85. package/src/interface/service/Observer/ERC20TransferObserver/ERC20TransferObserver.ts +181 -0
  86. package/src/interface/service/Observer/ERC20TransferObserver/index.ts +1 -0
  87. package/src/interface/service/Observer/ERC20TransferObserver/spec/ERC20TransferObserver.spec.ts +271 -0
  88. package/src/interface/service/Observer/LiquidityPoolBridgeObserver/LiquidityPoolBridgeObserver.ts +212 -0
  89. package/src/interface/service/Observer/LiquidityPoolBridgeObserver/index.ts +1 -0
  90. package/src/interface/service/Observer/LiquidityPoolBridgeObserver/spec/LiquidityPoolBridgeObserver.spec.ts +313 -0
  91. package/src/interface/service/Observer/Observer.ts +1 -1
  92. package/src/interface/service/{ChainBridgeRelay → Relay/ChainBridgeRelay}/ChainBridgeRelayInterface.ts +1 -1
  93. package/src/interface/service/{ChainBridgeRelay → Relay/ChainBridgeRelay}/ChainBridgeRelayService.ts +1 -1
  94. package/src/interface/service/{ChainBridgeRelay → Relay/ChainBridgeRelay}/spec/ChainBridgeRelayService.spec.ts +7 -5
  95. package/src/interface/service/Relay/LiquidityPoolBridgeRelay/LiquidityPoolBridgeRelay.ts +227 -0
  96. package/src/interface/service/Relay/LiquidityPoolBridgeRelay/index.ts +1 -0
  97. package/src/interface/service/Relay/LiquidityPoolBridgeRelay/spec/LiquidityPoolBridgeRelay.spec.ts +237 -0
  98. package/src/interface/service/Relay/index.ts +1 -0
  99. package/src/interface/service/index.ts +1 -1
  100. package/src/manifest/getLocator.ts +7 -6
  101. package/src/manifest/node.json +1 -1
  102. package/src/manifest/public/Chain.json +3 -109
  103. package/src/manifest/public/Ethereum.json +88 -0
  104. package/src/manifest/public/XL1.json +88 -0
  105. package/src/manifest/public/index.ts +15 -6
  106. package/src/server/app.ts +5 -12
  107. package/src/server/routes/addRoutes.ts +2 -6
  108. package/src/server/routes/bridge/addBridgeRoutes.ts +10 -0
  109. package/src/server/routes/bridge/index.ts +1 -0
  110. package/src/server/routes/bridge/middleware/index.ts +1 -0
  111. package/src/server/routes/bridge/middleware/requestHandlerValidator.ts +120 -0
  112. package/src/server/routes/bridge/routeDefinitions/getRouteDefinitions.ts +13 -0
  113. package/src/server/routes/bridge/routeDefinitions/index.ts +1 -0
  114. package/src/server/routes/bridge/routeDefinitions/pathParams/ChainIdPathParam.ts +18 -0
  115. package/src/server/routes/bridge/routeDefinitions/pathParams/index.ts +1 -0
  116. package/src/server/routes/bridge/routeDefinitions/routeDefinition.ts +18 -0
  117. package/src/server/routes/bridge/routeDefinitions/routes/bridgeFromRemoteStatus.ts +55 -0
  118. package/src/server/routes/bridge/routeDefinitions/routes/bridgeToRemote.ts +58 -0
  119. package/src/server/routes/bridge/routeDefinitions/routes/bridgeToRemoteEstimate.ts +83 -0
  120. package/src/server/routes/bridge/routeDefinitions/routes/bridgeToRemoteStatus.ts +55 -0
  121. package/src/server/routes/bridge/routeDefinitions/routes/index.ts +4 -0
  122. package/src/server/routes/healthz/get.ts +1 -1
  123. package/src/server/routes/index.ts +0 -2
  124. package/src/server/server.ts +11 -14
  125. package/dist/node/interface/service/ChainBridgeRelay/ChainBridgeRelayInterface.d.ts.map +0 -1
  126. package/dist/node/interface/service/ChainBridgeRelay/ChainBridgeRelayService.d.ts.map +0 -1
  127. package/dist/node/interface/service/ChainBridgeRelay/index.d.ts.map +0 -1
  128. package/dist/node/interface/service/ChainBridgeRelay/spec/ChainBridgeRelayService.spec.d.ts.map +0 -1
  129. package/dist/node/server/routes/rpc/index.d.ts +0 -2
  130. package/dist/node/server/routes/rpc/index.d.ts.map +0 -1
  131. package/dist/node/server/routes/rpc/routes/addRpcRoutes.d.ts +0 -3
  132. package/dist/node/server/routes/rpc/routes/addRpcRoutes.d.ts.map +0 -1
  133. package/dist/node/server/routes/rpc/routes/index.d.ts +0 -2
  134. package/dist/node/server/routes/rpc/routes/index.d.ts.map +0 -1
  135. package/src/manifest/public/Pending.json +0 -35
  136. package/src/server/routes/rpc/index.ts +0 -1
  137. package/src/server/routes/rpc/routes/addRpcRoutes.ts +0 -22
  138. package/src/server/routes/rpc/routes/index.ts +0 -1
  139. /package/dist/node/interface/service/{ChainBridgeRelay → Relay/ChainBridgeRelay}/index.d.ts +0 -0
  140. /package/dist/node/interface/service/{ChainBridgeRelay → Relay/ChainBridgeRelay}/spec/ChainBridgeRelayService.spec.d.ts +0 -0
  141. /package/src/interface/service/{ChainBridgeRelay → Relay/ChainBridgeRelay}/index.ts +0 -0
@@ -0,0 +1,181 @@
1
+ import type { Address } from '@xylabs/hex'
2
+ import {
3
+ asAddress, asHex, hexToBigInt, toAddress,
4
+ } from '@xylabs/hex'
5
+ import {
6
+ isBigInt, isNull, isUndefined,
7
+ } from '@xylabs/typeof'
8
+ import type { BridgeDestinationObservation, BridgeSourceObservation } from '@xyo-network/xl1-protocol'
9
+ import {
10
+ BridgeDestinationObservationSchema, BridgeSourceObservationSchema, XYO_ZERO_ADDRESS,
11
+ } from '@xyo-network/xl1-protocol'
12
+ import type { EventLog, WebSocketProvider } from 'ethers'
13
+ import { Contract } from 'ethers'
14
+ import { getAddress } from 'ethers/address'
15
+
16
+ import type { BridgeServiceParams } from '../../../interface/index.ts'
17
+ import { BridgeObserverService } from '../Observer.ts'
18
+
19
+ export type ERC20TransferObserverParams = BridgeServiceParams & {
20
+ /**
21
+ * An ethers.js WebSocketProvider connected to the Ethereum network to monitor for ERC-20 transfers.
22
+ */
23
+ provider: WebSocketProvider
24
+ /**
25
+ * The ERC-20 token contract address to monitor (e.g., XYO, USDC).
26
+ */
27
+ tokenAddress: string
28
+ /**
29
+ * The address to watch for incoming or outgoing ERC-20 token transfers.
30
+ */
31
+ watchAddress: string
32
+ /**
33
+ * Specify whether to watch for 'incoming' or 'outgoing' types of transfers.
34
+ */
35
+ watchDirection: 'incoming' | 'outgoing'
36
+ }
37
+
38
+ // Minimal ERC-20 ABI only with Transfer event
39
+ const ERC20_ABI = [
40
+ 'event Transfer(address indexed from, address indexed to, uint256 value)',
41
+ ]
42
+
43
+ export class ERC20TransferObserver extends BridgeObserverService<ERC20TransferObserverParams> {
44
+ protected get tokenAddress(): Address {
45
+ return toAddress(this.params.tokenAddress)
46
+ }
47
+
48
+ override async createHandler(): Promise<void> {
49
+ const {
50
+ provider, tokenAddress, watchAddress, watchDirection,
51
+ } = this.params
52
+ // The custodial wallet to watch for transfers
53
+ const normalizedWatchAddress = getAddress(watchAddress)
54
+
55
+ // Create a Contract for the ERC-20 token
56
+ const token = new Contract(getAddress(tokenAddress), ERC20_ABI, provider)
57
+
58
+ // Listen for transfers involving WATCH_ADDRESS
59
+ const filterIncoming = token.filters.Transfer(null, normalizedWatchAddress)
60
+ const filterOutgoing = token.filters.Transfer(normalizedWatchAddress, null)
61
+
62
+ // Pick correct filter + direction
63
+ const filter = watchDirection === 'incoming' ? filterIncoming : filterOutgoing
64
+
65
+ // Replay old logs
66
+ const currentBlock = await provider.getBlockNumber()
67
+ const pastEvents = await token.queryFilter(filter, 0, currentBlock)
68
+ for (const ev of pastEvents) {
69
+ const {
70
+ from, to, value,
71
+ } = (ev as EventLog)?.args
72
+ await (watchDirection === 'incoming' ? this.handleTransfer(from, value, 'incoming') : this.handleTransfer(to, value, 'outgoing'))
73
+ }
74
+
75
+ // Watch for new events
76
+ if (watchDirection === 'incoming') {
77
+ await token.on(filterIncoming, (ev: EventLog) => {
78
+ // Sanitize event log
79
+ const { from, value } = ev.args
80
+ if (isUndefined(from) || isUndefined(value)) return
81
+ const sender = asAddress(from)
82
+ // Ignore mints
83
+ if (isUndefined(sender) || sender === XYO_ZERO_ADDRESS) return
84
+ // Ensure value is bigint and non-zero
85
+ if (!isBigInt(value) || value === 0n) return
86
+ // Handle transfer
87
+ this.handleTransfer(sender, value, 'incoming').catch(console.error)
88
+ })
89
+ } else if (watchDirection === 'outgoing') {
90
+ await token.on(filterOutgoing, (ev: EventLog) => {
91
+ // Sanitize event log
92
+ const { to, value } = ev.args
93
+ if (isUndefined(to) || isUndefined(value)) return
94
+ const receiver = asAddress(to)
95
+ // Ignore burns
96
+ if (isUndefined(receiver) || receiver === XYO_ZERO_ADDRESS) return
97
+ // Ensure value is bigint and non-zero
98
+ if (!isBigInt(value) || value === 0n) return
99
+ this.handleTransfer(receiver, value, 'outgoing').catch(console.error)
100
+ })
101
+ } else {
102
+ throw new Error(`Invalid watchDirection: ${watchDirection}`)
103
+ }
104
+ }
105
+
106
+ private async handleTransfer(account: Address, value: bigint, direction: 'incoming' | 'outgoing'): Promise<void> {
107
+ if (direction === 'incoming') {
108
+ const sender = asAddress(account)
109
+ if (isUndefined (sender) || sender === XYO_ZERO_ADDRESS) return
110
+ const intents = await this.intents.getIntentsForSource(sender)
111
+ if (intents.length === 0) return
112
+ // Find the intent that matches the transfer amount (chain and token are implicit to provider and intents were already indexed by address)
113
+ const intent = intents.findLast((i) => {
114
+ try {
115
+ // Source address matches intent
116
+ const address = asAddress(i.srcAddress)
117
+ if (isUndefined(address) || address !== sender) return false
118
+ // Source token must match
119
+ const token = asHex(i.srcToken)
120
+ if (isUndefined(token) || token !== this.tokenAddress) return false
121
+ // Source amount must match
122
+ const hexAmount = asHex(i.srcAmount)
123
+ if (isUndefined(hexAmount)) return false
124
+ const amount = hexToBigInt(hexAmount)
125
+ if (amount !== value) return false
126
+ // Otherwise matches
127
+ return true
128
+ } catch {
129
+ return false
130
+ }
131
+ })
132
+ if (isUndefined(intent)) return
133
+ // Found matching intent, check if observation already exists
134
+ const observations = this.sourceObservations
135
+ const existing = await observations.getObservationForIntent(intent)
136
+ // Only add if observation not already existing
137
+ if (isUndefined(existing) || isNull(existing)) {
138
+ const { schema, ...rest } = intent
139
+ const observation: BridgeSourceObservation = { schema: BridgeSourceObservationSchema, ...rest }
140
+ await observations.addObservation(observation, intent)
141
+ }
142
+ } else if (direction === 'outgoing') {
143
+ const receiver = asAddress(account)
144
+ if (isUndefined (receiver) || receiver === XYO_ZERO_ADDRESS) return
145
+ const intents = await this.intents.getIntentsForDestination(receiver)
146
+ if (intents.length === 0) return
147
+ // Find the intent that matches the transfer amount (chain and token are implicit to provider and intents were already indexed by address)
148
+ const intent = intents.findLast((i) => {
149
+ try {
150
+ // Source address matches intent
151
+ const address = asAddress(i.destAddress)
152
+ if (isUndefined(address) || address !== receiver) return false
153
+ // Source token must match
154
+ const token = asHex(i.destToken)
155
+ if (isUndefined(token) || token !== this.tokenAddress) return false
156
+ // Source amount must match
157
+ const hexAmount = asHex(i.destAmount)
158
+ if (isUndefined(hexAmount)) return false
159
+ const amount = hexToBigInt(hexAmount)
160
+ if (amount !== value) return false
161
+ // Otherwise matches
162
+ return true
163
+ } catch {
164
+ return false
165
+ }
166
+ })
167
+ if (isUndefined(intent)) return
168
+ // Found matching intent, check if observation already exists
169
+ const observations = this.destinationObservations
170
+ const existing = await observations.getObservationForIntent(intent)
171
+ // Only add if observation not already existing
172
+ if (isUndefined(existing) || isNull(existing)) {
173
+ const { schema, ...rest } = intent
174
+ const observation: BridgeDestinationObservation = { schema: BridgeDestinationObservationSchema, ...rest }
175
+ await observations.addObservation(observation, intent)
176
+ }
177
+ } else {
178
+ throw new Error(`Invalid direction: ${direction}`)
179
+ }
180
+ }
181
+ }
@@ -0,0 +1 @@
1
+ export * from './ERC20TransferObserver.ts'
@@ -0,0 +1,271 @@
1
+ import { assertEx } from '@xylabs/assert'
2
+ import { delay } from '@xylabs/delay'
3
+ import type { Address } from '@xylabs/hex'
4
+ import { toAddress, toHex } from '@xylabs/hex'
5
+ import { Account } from '@xyo-network/account'
6
+ import type { BridgeableToken } from '@xyo-network/typechain'
7
+ import { BridgeableToken__factory } from '@xyo-network/typechain'
8
+ import {
9
+ AttoXL1ConvertFactor, type BridgeIntent, BridgeIntentSchema, type ChainId,
10
+ } from '@xyo-network/xl1-protocol'
11
+ import {
12
+ parseEther, Wallet, WebSocketProvider,
13
+ } from 'ethers'
14
+ import {
15
+ beforeAll, beforeEach, describe, expect, it, vi,
16
+ } from 'vitest'
17
+
18
+ import type {
19
+ BridgeDestinationObservationIndexerInterface, BridgeIntentIndexerInterface, BridgeSourceObservationIndexerInterface,
20
+ } from '../../../../interface/index.ts'
21
+ import { ERC20TransferObserver } from '../ERC20TransferObserver.ts'
22
+
23
+ describe.skip('ERC20TransferObserver', () => {
24
+ // Test ERC-20 deployed to Hardhat local chain
25
+ const TOKEN_ADDRESS = '0x5FbDB2315678afecb367f032d93F642f64180aa3'
26
+ const WS_URL = 'ws://127.0.0.1:8545'
27
+ const provider = new WebSocketProvider(WS_URL)
28
+ let ethBridgeSender: Wallet
29
+ let ethBridgeReceiver: Wallet
30
+ let custodialWallet: Wallet
31
+ let token: BridgeableToken
32
+ let xl1Address: Address
33
+ let xl1ChainId: ChainId
34
+ let sourceObservations: BridgeSourceObservationIndexerInterface
35
+ let destinationObservations: BridgeDestinationObservationIndexerInterface
36
+ let intents: BridgeIntentIndexerInterface
37
+
38
+ const srcAmountBigint = 100n * AttoXL1ConvertFactor.xl1
39
+ const srcAmount = toHex(srcAmountBigint)
40
+ const destAmount = srcAmount // 1:1 for test
41
+ const ethChainId = toHex('0x7A69')
42
+ const bridgeableTokenContract = toHex(TOKEN_ADDRESS)
43
+
44
+ const createRandomWallet = async (): Promise<Wallet> => {
45
+ // Create random account
46
+ const account = await Account.random()
47
+ expect(account.private?.hex).toBeDefined()
48
+ const key = assertEx(account.private?.hex)
49
+
50
+ // Create a wallet from the private key
51
+ const wallet = new Wallet(key, provider)
52
+ const deployer = await provider.getSigner(0)
53
+
54
+ // Fund the wallet with some ETH for gas
55
+ const fundTx = await deployer.sendTransaction({ to: wallet.address, value: parseEther('1') })
56
+ await fundTx.wait()
57
+
58
+ // Ensure wallet has ETH
59
+ const balance = await provider.getBalance(wallet.address)
60
+ expect(balance).toBeGreaterThan(0n)
61
+
62
+ // Return the created wallet
63
+ return wallet
64
+ }
65
+
66
+ beforeAll(async () => {
67
+ const tokenOwner = await provider.getSigner(0)
68
+ ethBridgeSender = await createRandomWallet()
69
+ ethBridgeReceiver = await createRandomWallet()
70
+ custodialWallet = await createRandomWallet()
71
+
72
+ token = BridgeableToken__factory.connect(TOKEN_ADDRESS, tokenOwner)
73
+ await token.mint(custodialWallet.address, parseEther((srcAmountBigint * 2n).toString()))
74
+ await token.mint(ethBridgeSender.address, parseEther((srcAmountBigint * 2n).toString()))
75
+ })
76
+
77
+ beforeEach(async () => {
78
+ xl1Address = (await Account.random()).address
79
+ xl1ChainId = (await Account.random()).address
80
+ })
81
+
82
+ describe('when bridging from Ethereum', () => {
83
+ beforeEach(() => {
84
+ sourceObservations = {
85
+ addObservation: vi.fn().mockResolvedValue(true),
86
+ getObservationForIntent: vi.fn().mockResolvedValue(null),
87
+ getIntentForObservation: vi.fn().mockResolvedValue(null),
88
+ }
89
+ destinationObservations = {
90
+ addObservation: vi.fn().mockResolvedValue(true),
91
+ getObservationForIntent: vi.fn().mockResolvedValue(null),
92
+ getIntentForObservation: vi.fn().mockResolvedValue(null),
93
+ }
94
+ const nonce = Date.now().toString()
95
+ const intent: BridgeIntent = {
96
+ // Source
97
+ src: ethChainId,
98
+ srcAddress: toAddress(ethBridgeSender.address),
99
+ srcAmount,
100
+ srcToken: bridgeableTokenContract,
101
+
102
+ // Destination
103
+ dest: xl1ChainId,
104
+ destAddress: xl1Address,
105
+ destAmount,
106
+ destToken: xl1ChainId,
107
+
108
+ // Details
109
+ nonce,
110
+
111
+ schema: BridgeIntentSchema,
112
+ }
113
+ intents = {
114
+ addIntent: vi.fn().mockResolvedValue(true),
115
+ getIntentByNonce: vi.fn().mockResolvedValue(intent),
116
+ getIntentsForDestination: vi.fn().mockResolvedValue([]),
117
+ getIntentsForSource: vi.fn().mockResolvedValue([intent]),
118
+ }
119
+ })
120
+ describe('for new transfers to custodial wallet', () => {
121
+ it('should observe transfer', async () => {
122
+ // Arrange
123
+ // Create observer before transfer
124
+ const observer = await ERC20TransferObserver.create({
125
+ destinationObservations,
126
+ intents,
127
+ provider,
128
+ sourceObservations,
129
+ tokenAddress: TOKEN_ADDRESS,
130
+ watchAddress: toAddress(custodialWallet.address),
131
+ watchDirection: 'incoming',
132
+ })
133
+ expect(observer).toBeDefined()
134
+ // Ensure sender has tokens
135
+ const balance = await token.balanceOf(ethBridgeSender.address)
136
+ expect(balance).toBeGreaterThan(0n)
137
+ const sender = token.connect(ethBridgeSender)
138
+
139
+ // Act
140
+ // Transfer from sender to custodial wallet
141
+ await sender.transfer(custodialWallet.address, srcAmountBigint)
142
+
143
+ // Wait for event
144
+ await delay(2000)
145
+
146
+ // Assert
147
+ // Ensure transfer was observed
148
+ expect(sourceObservations.addObservation).toHaveBeenCalled()
149
+ })
150
+ })
151
+ describe('for previous transfers to custodial wallet', () => {
152
+ it('should observe transfer', async () => {
153
+ // Arrange
154
+ // Ensure sender has tokens
155
+ const balance = await token.balanceOf(ethBridgeSender.address)
156
+ expect(balance).toBeGreaterThan(0n)
157
+ const sender = token.connect(ethBridgeSender)
158
+ // Transfer from sender to custodial wallet
159
+ await sender.transfer(custodialWallet.address, srcAmountBigint)
160
+
161
+ // Act
162
+ // Create observer after transfer
163
+ const observer = await ERC20TransferObserver.create({
164
+ destinationObservations,
165
+ intents,
166
+ provider,
167
+ sourceObservations,
168
+ tokenAddress: TOKEN_ADDRESS,
169
+ watchAddress: toAddress(custodialWallet.address),
170
+ watchDirection: 'incoming',
171
+ })
172
+ expect(observer).toBeDefined()
173
+
174
+ // Assert
175
+ // Ensure transfer was observed
176
+ expect(sourceObservations.addObservation).toHaveBeenCalled()
177
+ })
178
+ })
179
+ })
180
+
181
+ describe('when bridging to Ethereum', () => {
182
+ beforeEach(() => {
183
+ sourceObservations = {
184
+ addObservation: vi.fn().mockResolvedValue(true),
185
+ getObservationForIntent: vi.fn().mockResolvedValue(null),
186
+ getIntentForObservation: vi.fn().mockResolvedValue(null),
187
+ }
188
+ destinationObservations = {
189
+ addObservation: vi.fn().mockResolvedValue(true),
190
+ getObservationForIntent: vi.fn().mockResolvedValue(null),
191
+ getIntentForObservation: vi.fn().mockResolvedValue(null),
192
+ }
193
+ const nonce = Date.now().toString()
194
+ const intent: BridgeIntent = {
195
+ // Source
196
+ src: xl1ChainId, // From XL1
197
+ srcAddress: xl1Address, // From XL1 sender
198
+ srcAmount,
199
+ srcToken: xl1ChainId, // In XL1
200
+
201
+ // Destination
202
+ dest: ethChainId, // To Ethereum
203
+ destAddress: toAddress(ethBridgeReceiver.address),
204
+ destAmount,
205
+ destToken: bridgeableTokenContract,
206
+
207
+ // Details
208
+ nonce,
209
+
210
+ schema: BridgeIntentSchema,
211
+ }
212
+ intents = {
213
+ addIntent: vi.fn().mockResolvedValue(true),
214
+ getIntentByNonce: vi.fn().mockResolvedValue(intent),
215
+ getIntentsForDestination: vi.fn().mockResolvedValue([intent]),
216
+ getIntentsForSource: vi.fn().mockResolvedValue([]),
217
+ }
218
+ })
219
+ describe('for new transfers from custodial wallet', () => {
220
+ it('should observe transfer', async () => {
221
+ // Arrange
222
+ // Create observer before transfer
223
+ const observer = await ERC20TransferObserver.create({
224
+ destinationObservations,
225
+ intents,
226
+ provider,
227
+ sourceObservations,
228
+ tokenAddress: TOKEN_ADDRESS,
229
+ watchAddress: toAddress(custodialWallet.address),
230
+ watchDirection: 'outgoing',
231
+ })
232
+ expect(observer).toBeDefined()
233
+
234
+ // Act
235
+ const sender = token.connect(custodialWallet)
236
+ await sender.transfer(ethBridgeReceiver.address, srcAmountBigint.toString())
237
+
238
+ // Wait for event
239
+ await delay(2000)
240
+
241
+ // Assert
242
+ // Ensure transfer was observed
243
+ expect(destinationObservations.addObservation).toHaveBeenCalled()
244
+ })
245
+ })
246
+ describe('for previous transfers from custodial wallet', () => {
247
+ it('should observe transfer', async () => {
248
+ // Arrange
249
+ const sender = token.connect(custodialWallet)
250
+ await sender.transfer(ethBridgeReceiver.address, srcAmountBigint.toString())
251
+
252
+ // Act
253
+ // Create observer after transfer
254
+ const observer = await ERC20TransferObserver.create({
255
+ destinationObservations,
256
+ intents,
257
+ provider,
258
+ sourceObservations,
259
+ tokenAddress: TOKEN_ADDRESS,
260
+ watchAddress: toAddress(custodialWallet.address),
261
+ watchDirection: 'outgoing',
262
+ })
263
+ expect(observer).toBeDefined()
264
+
265
+ // Assert
266
+ // Ensure transfer was observed
267
+ expect(destinationObservations.addObservation).toHaveBeenCalled()
268
+ })
269
+ })
270
+ })
271
+ })
@@ -0,0 +1,212 @@
1
+ import { assertEx } from '@xylabs/assert'
2
+ import type { Address, Hex } from '@xylabs/hex'
3
+ import {
4
+ asAddress, asHex, hexFromBigInt, hexToBigInt, toAddress,
5
+ } from '@xylabs/hex'
6
+ import { isNull, isUndefined } from '@xylabs/typeof'
7
+ import { PayloadBuilder } from '@xyo-network/payload-builder'
8
+ import type { LiquidityPoolBridge } from '@xyo-network/typechain'
9
+ import { LiquidityPoolBridge__factory } from '@xyo-network/typechain'
10
+ import type {
11
+ BridgeDestinationObservation, BridgeIntent, BridgeSourceObservation,
12
+ } from '@xyo-network/xl1-protocol'
13
+ import {
14
+ BridgeDestinationObservationSchema, BridgeIntentSchema, BridgeSourceObservationSchema,
15
+ } from '@xyo-network/xl1-protocol'
16
+ import type { ContractEventPayload, WebSocketProvider } from 'ethers'
17
+ import { getAddress } from 'ethers'
18
+
19
+ import type { BridgeServiceParams } from '../../../interface/index.ts'
20
+ import { BridgeObserverService } from '../Observer.ts'
21
+
22
+ export type LiquidityPoolBridgeObserverParams = BridgeServiceParams & {
23
+ /**
24
+ * The address to watch for incoming or outgoing ERC-20 token transfers.
25
+ */
26
+ bridgeAddress: string
27
+ /**
28
+ * An ethers.js WebSocketProvider connected to the Ethereum network to monitor for ERC-20 transfers.
29
+ */
30
+ provider: WebSocketProvider
31
+ /**
32
+ * The block number to start monitoring from.
33
+ */
34
+ startBlock?: number
35
+ }
36
+
37
+ export class LiquidityPoolBridgeObserver extends BridgeObserverService<LiquidityPoolBridgeObserverParams> {
38
+ protected _bridge: LiquidityPoolBridge | undefined
39
+ protected _bridgeChainId: Hex | undefined
40
+ protected _bridgeRemoteChainId: Hex | undefined
41
+ protected _bridgeTokenAddress: Address | undefined
42
+
43
+ protected get bridge(): LiquidityPoolBridge {
44
+ return assertEx(this._bridge, () => new Error('Bridge contract not initialized'))
45
+ }
46
+
47
+ protected get bridgeChainId(): Hex {
48
+ return assertEx(this._bridgeChainId, () => new Error('Bridge chain ID not initialized'))
49
+ }
50
+
51
+ protected get bridgeRemoteChainId(): Hex {
52
+ return assertEx(this._bridgeRemoteChainId, () => new Error('Bridge remote chain ID not initialized'))
53
+ }
54
+
55
+ protected get bridgeTokenAddress(): Address {
56
+ return assertEx(this._bridgeTokenAddress, () => new Error('Bridge token address not initialized'))
57
+ }
58
+
59
+ protected get provider(): WebSocketProvider {
60
+ return assertEx(this.params.provider, () => new Error('Provider not initialized'))
61
+ }
62
+
63
+ protected get startBlock(): number {
64
+ return isUndefined(this.params.startBlock) ? 0 : this.params.startBlock
65
+ }
66
+
67
+ override async createHandler(): Promise<void> {
68
+ const { provider, bridgeAddress } = this.params
69
+
70
+ // Connect to the bridge contract
71
+ this._bridge = LiquidityPoolBridge__factory.connect(getAddress(bridgeAddress), provider)
72
+
73
+ // Parse bridge network chain ID
74
+ const network = await provider.getNetwork()
75
+ this._bridgeChainId = assertEx(hexFromBigInt(network.chainId), () => new Error('Failed to parse bridgeChainId'))
76
+
77
+ // Parse bridge token address
78
+ const tokenAddress = await this.bridge.token()
79
+ this._bridgeTokenAddress = toAddress(tokenAddress)
80
+
81
+ // Parse bridge remote chain ID
82
+ const bridgeRemoteChain = await this.bridge.remoteChain()
83
+ this._bridgeRemoteChainId = asHex(bridgeRemoteChain)
84
+
85
+ // Grab the current block number to avoid processing old events
86
+ const currentBlock = await provider.getBlockNumber()
87
+ const manualIndexThroughBlockNumber = Math.max(currentBlock, this.startBlock)
88
+
89
+ // Watch for & process new events
90
+ await this.bridge.on(this.bridge.getEvent('BridgedToRemote'), (id, from, to, amount, remoteChain, args) => {
91
+ const contractEvent = args as unknown as ContractEventPayload
92
+ if (contractEvent?.log?.blockNumber <= manualIndexThroughBlockNumber) return
93
+ this.handleBridgeToRemote(id, from, to, amount, remoteChain).catch(console.error)
94
+ })
95
+ await this.bridge.on(this.bridge.getEvent('BridgedFromRemote'), (id, from, to, amount, remoteChain, args) => {
96
+ const contractEvent = args as unknown as ContractEventPayload
97
+ if (contractEvent?.log?.blockNumber <= manualIndexThroughBlockNumber) return
98
+ this.handleBridgeFromRemote(id, from, to, amount, remoteChain).catch(console.error)
99
+ })
100
+
101
+ // Process old events
102
+ await this.processOldEvents(this.startBlock, manualIndexThroughBlockNumber)
103
+ }
104
+
105
+ private async handleBridgeFromRemote(id: bigint, from: string, to: string, value: bigint, remoteChain: string): Promise<void> {
106
+ const srcAddress = asAddress(from)
107
+ const destAddress = asAddress(to)
108
+ const remoteChainId = asHex(remoteChain)
109
+ if (isUndefined (srcAddress) || isUndefined (destAddress) || isUndefined (remoteChainId)) return
110
+ if (remoteChainId != this.bridgeRemoteChainId) return
111
+
112
+ const intents = await this.intents.getIntentsForDestination(destAddress)
113
+ if (intents.length === 0) return
114
+ // Find the intent that matches the transfer amount (chain and token are implicit to provider and intents were already indexed by address)
115
+ const intent = intents.findLast((i) => {
116
+ try {
117
+ // TODO: Add source to event signature to capture intent
118
+ // Source address matches intent
119
+ // const intentSrcAddress = asAddress(i.srcAddress)
120
+ // if (isUndefined(intentSrcAddress) || intentSrcAddress !== srcAddress) return false
121
+ // Destination address matches intent
122
+ const intentDestAddress = asAddress(i.destAddress)
123
+ if (isUndefined(intentDestAddress) || intentDestAddress !== destAddress) return false
124
+
125
+ // Source token matches intent
126
+ const intentSrcToken = i.srcToken
127
+ if (isUndefined(intentSrcToken) || intentSrcToken !== this.bridgeRemoteChainId) return false
128
+ // Destination token matches intent
129
+ const intentDestToken = asHex(i.destToken)
130
+ if (isUndefined(intentDestToken) || intentDestToken !== this.bridgeTokenAddress) return false
131
+
132
+ // Destination amount must match
133
+ const intentDestAmountHex = asHex(i.destAmount)
134
+ if (isUndefined(intentDestAmountHex)) return false
135
+ const intentDestAmountBigInt = hexToBigInt(intentDestAmountHex)
136
+ if (intentDestAmountBigInt !== value) return false
137
+
138
+ // Otherwise matches
139
+ return true
140
+ } catch {
141
+ return false
142
+ }
143
+ })
144
+
145
+ if (isUndefined(intent)) return
146
+ // Found matching intent, check if observation already exists
147
+ const observations = this.destinationObservations
148
+ const existing = await observations.getObservationForIntent(intent)
149
+ // Only add if observation not already existing
150
+ if (isUndefined(existing) || isNull(existing)) {
151
+ const { schema, ...rest } = intent
152
+ const observation: BridgeDestinationObservation = { schema: BridgeDestinationObservationSchema, ...rest }
153
+ await observations.addObservation(observation, intent)
154
+ }
155
+ }
156
+
157
+ private async handleBridgeToRemote(id: bigint, from: string, to: string, value: bigint, remoteChain: string): Promise<void> {
158
+ const srcAddress = asAddress(from)
159
+ const destAddress = asAddress(to)
160
+ const remoteChainId = asHex(remoteChain)
161
+ if (isUndefined (srcAddress) || isUndefined (destAddress) || isUndefined (remoteChainId)) return
162
+ if (remoteChainId != this.bridgeRemoteChainId) return
163
+
164
+ // If we don't have an intent for this nonce already
165
+ const nonce = hexFromBigInt(id)
166
+ let intent = await this.intents.getIntentByNonce(nonce)
167
+ if (isUndefined(intent)) {
168
+ // Create observation for intent if none exists
169
+ intent = new PayloadBuilder<BridgeIntent>({ schema: BridgeIntentSchema }).fields({
170
+ nonce,
171
+ dest: this.bridgeRemoteChainId,
172
+ destAddress,
173
+ destAmount: hexFromBigInt(value),
174
+ destToken: this.bridgeRemoteChainId,
175
+ src: this.bridgeChainId,
176
+ srcAddress,
177
+ srcAmount: hexFromBigInt(value),
178
+ srcToken: this.bridgeTokenAddress,
179
+ }).build()
180
+ await this.intents.addIntent(intent)
181
+ }
182
+ // Ensure we have an intent to match against
183
+ if (isUndefined(intent)) return
184
+ // Found matching intent, check if observation already exists
185
+ const observations = this.sourceObservations
186
+ const existing = await observations.getObservationForIntent(intent)
187
+ // Only add if observation not already existing
188
+ if (isUndefined(existing) || isNull(existing)) {
189
+ const { schema, ...rest } = intent
190
+ const observation: BridgeSourceObservation = { schema: BridgeSourceObservationSchema, ...rest }
191
+ await observations.addObservation(observation, intent)
192
+ }
193
+ }
194
+
195
+ private async processOldEvents(startBlock: number, endBlock: number): Promise<void> {
196
+ const bridgedToRemote = await this.bridge.queryFilter(this.bridge.filters.BridgedToRemote(), startBlock, endBlock)
197
+ for (const log of bridgedToRemote) {
198
+ const {
199
+ id, srcAddress, destAddress, amount, destToken,
200
+ } = log.args
201
+ await this.handleBridgeToRemote(id, srcAddress, destAddress, amount, destToken)
202
+ }
203
+
204
+ const bridgedFromRemote = await this.bridge.queryFilter(this.bridge.filters.BridgedFromRemote(), startBlock, endBlock)
205
+ for (const log of bridgedFromRemote) {
206
+ const {
207
+ id, srcAddress, destAddress, amount, srcToken,
208
+ } = log.args
209
+ await this.handleBridgeFromRemote(id, srcAddress, destAddress, amount, srcToken)
210
+ }
211
+ }
212
+ }
@@ -0,0 +1 @@
1
+ export * from './LiquidityPoolBridgeObserver.ts'