Skip to content

Commit 97e8dde

Browse files
authored
Merge pull request #36 from smartcontractkit/txwait
2 parents a4b93f3 + 74ce9c3 commit 97e8dde

File tree

5 files changed

+6008
-9
lines changed

5 files changed

+6008
-9
lines changed

.changeset/three-seas-beam.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@chainlink/functions-toolkit': patch
3+
---
4+
5+
Added ResponseListener.listenForResponseFromTransaction() method to handle listening for responses if the request was reorged and the requestId changed

README.md

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -450,9 +450,27 @@ provider,
450450
functionsRouterAddress,
451451
})
452452

453-
To listen for the response to a single Functions request by request ID, use the the `listenForResponse()` method. Optionally, you can provide a custom timeout after which the listener will throw an error indicating that the time limit was exceeded. If no timeout is provided, the default timeout is 300 seconds.
453+
To listen for a response to a single Functions request, use the `listenForResponseFromTransaction()` method.
454+
Optionally, you can provide:
455+
- timeout after which the listener will throw an error indicating that the time limit was exceeded (default 5 minutes)
456+
- number of block confirmations (default 2)
457+
- frequency of checking if the request is already included on-chain (or if it got moved after a chain re-org) (default 2 seconds)
454458

455-
**Note:** Listening for multiple responses simultaneously is not supported by the `listenForResponse()` method and will lead to undefined behavior.
459+
```
460+
const response: FunctionsResponse = await responseListener.listenForResponseFromTransaction(
461+
txHash: string,
462+
timeout?: number,
463+
confirmations?: number,
464+
checkInterval?: number,
465+
)
466+
```
467+
468+
Alternatively, to listen using a request ID, use the `listenForResponse()` method.
469+
470+
**Notes:**
471+
1. Request ID can change during a chain re-org so it's less reliable than a request transaction hash.
472+
2. If the methods are called after the response is already on chain, it won't be returned correctly.
473+
3. Listening for multiple responses simultaneously is not supported by the above methods and will lead to undefined behavior.
456474

457475
```
458476
const response: FunctionsResponse = await responseListener.listenForResponse(
@@ -479,13 +497,9 @@ The possible fulfillment codes are shown below.
479497

480498
```
481499
{
482-
FULFILLED = 0, // Indicates that calling the consumer contract's handleOracleFulfill method was successful
500+
FULFILLED = 0, // Indicates that a Function was executed and calling the consumer contract's handleOracleFulfill method was successful
483501
USER_CALLBACK_ERROR = 1, // Indicates that the consumer contract's handleOracleFulfill method reverted
484-
INVALID_REQUEST_ID = 2, // Internal error
485-
COST_EXCEEDS_COMMITMENT = 3, // Indicates that the request was not fulfilled because the cost of fulfillment is higher than the estimated cost due to an increase in gas prices
486-
INSUFFICIENT_GAS_PROVIDED = 4, // Internal error
487-
SUBSCRIPTION_BALANCE_INVARIANT_VIOLATION, // Internal error
488-
INVALID_COMMITMENT = 6, // Internal error
502+
// all other codes indicate internal errors
489503
}
490504
```
491505

src/ResponseListener.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { FulfillmentCode, type FunctionsResponse } from './types'
88

99
export class ResponseListener {
1010
private functionsRouter: Contract
11+
private provider: providers.Provider
1112

1213
constructor({
1314
provider,
@@ -16,6 +17,7 @@ export class ResponseListener {
1617
provider: providers.Provider
1718
functionsRouterAddress: string
1819
}) {
20+
this.provider = provider
1921
this.functionsRouter = new Contract(functionsRouterAddress, FunctionsRouterSource.abi, provider)
2022
}
2123

@@ -55,7 +57,45 @@ export class ResponseListener {
5557
)
5658
})
5759

58-
return await responsePromise
60+
return responsePromise
61+
}
62+
63+
public async listenForResponseFromTransaction(
64+
txHash: string,
65+
timeout = 3000000,
66+
confirmations = 2,
67+
checkInterval = 2000,
68+
): Promise<FunctionsResponse> {
69+
return new Promise<FunctionsResponse>((resolve, reject) => {
70+
;(async () => {
71+
let requestID: string
72+
// eslint-disable-next-line prefer-const
73+
let checkTimeout: NodeJS.Timeout
74+
const expirationTimeout = setTimeout(() => {
75+
reject('Response not received within timeout period')
76+
}, timeout)
77+
78+
const check = async () => {
79+
const receipt = await this.provider.waitForTransaction(txHash, confirmations, timeout)
80+
const updatedID = receipt.logs[0].topics[1]
81+
if (updatedID !== requestID) {
82+
requestID = updatedID
83+
const response = await this.listenForResponse(receipt.logs[0].topics[1], timeout)
84+
if (updatedID === requestID) {
85+
// Resolve only if the ID hasn't changed in the meantime
86+
clearTimeout(expirationTimeout)
87+
clearInterval(checkTimeout)
88+
resolve(response)
89+
}
90+
}
91+
}
92+
93+
// Check periodically if the transaction has been re-orged and requestID changed
94+
checkTimeout = setInterval(check, checkInterval)
95+
96+
check()
97+
})()
98+
})
5999
}
60100

61101
public listenForResponses(

test/integration/ResponseListener.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,78 @@ describe('Functions toolkit classes', () => {
101101
expect(errResponse.fulfillmentCode).toBe(FulfillmentCode.FULFILLED)
102102
})
103103

104+
it('Successfully waits for single response from transaction hash', async () => {
105+
const subscriptionManager = new SubscriptionManager({
106+
signer: allowlistedUser_A,
107+
linkTokenAddress,
108+
functionsRouterAddress,
109+
})
110+
await subscriptionManager.initialize()
111+
112+
const subscriptionId = await subscriptionManager.createSubscription()
113+
await subscriptionManager.fundSubscription({
114+
juelsAmount: utils.parseUnits('1', 'ether').toString(),
115+
subscriptionId,
116+
})
117+
await subscriptionManager.addConsumer({
118+
subscriptionId,
119+
consumerAddress: exampleClient.address,
120+
txOptions: {
121+
confirmations: 1,
122+
},
123+
})
124+
125+
const functionsListener = new ResponseListener({
126+
provider: allowlistedUser_A.provider,
127+
functionsRouterAddress,
128+
})
129+
130+
const succReqTx = await exampleClient.sendRequest(
131+
'return Functions.encodeUint256(1)',
132+
1,
133+
[],
134+
[],
135+
[],
136+
subscriptionId,
137+
100_000,
138+
)
139+
140+
const succReq = await succReqTx.wait()
141+
const succResponse = await functionsListener.listenForResponseFromTransaction(
142+
succReq.transactionHash,
143+
1000000,
144+
0,
145+
)
146+
147+
expect(succResponse.responseBytesHexstring).toBe(
148+
'0x0000000000000000000000000000000000000000000000000000000000000001',
149+
)
150+
expect(succResponse.errorString).toBe('')
151+
expect(succResponse.returnDataBytesHexstring).toBe('0x')
152+
expect(succResponse.fulfillmentCode).toBe(FulfillmentCode.FULFILLED)
153+
154+
const errReqTx = await exampleClient.sendRequest(
155+
'return Functions.encodeUint256(1',
156+
1,
157+
[],
158+
[],
159+
[],
160+
subscriptionId,
161+
100_000,
162+
)
163+
164+
const errReq = await errReqTx.wait(1)
165+
const errRequestId = errReq.events[0].topics[1]
166+
167+
const errResponse = await functionsListener.listenForResponse(errRequestId)
168+
169+
expect(errResponse.requestId).toBe(errRequestId)
170+
expect(errResponse.responseBytesHexstring).toBe('0x')
171+
expect(errResponse.errorString).toBe('syntax error, RAM exceeded, or other error')
172+
expect(errResponse.returnDataBytesHexstring).toBe('0x')
173+
expect(errResponse.fulfillmentCode).toBe(FulfillmentCode.FULFILLED)
174+
})
175+
104176
it('Successfully listens for responses', async () => {
105177
const subscriptionManager = new SubscriptionManager({
106178
signer: allowlistedUser_A,

0 commit comments

Comments
 (0)