Skip to content

Commit

Permalink
feat: implementation of signature status check after setting up signa…
Browse files Browse the repository at this point in the history
…ture subscription
  • Loading branch information
steveluscher committed Oct 21, 2022
1 parent 648e83e commit 78b36ee
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 111 deletions.
98 changes: 52 additions & 46 deletions web3.js/src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3386,18 +3386,21 @@ export class Connection {

const subscriptionCommitment = commitment || this.commitment;
let timeoutId;
let subscriptionId;
let signatureSubscriptionId: number | undefined;
let disposeSignatureSubscriptionStateChangeObserver:
| SubscriptionStateChangeDisposeFn
| undefined;
let done = false;

const confirmationPromise = new Promise<{
__type: TransactionStatus.PROCESSED;
response: RpcResponseAndContext<SignatureResult>;
}>((resolve, reject) => {
let intervalId: null | NodeJS.Timer = null;
try {
subscriptionId = this.onSignature(
signatureSubscriptionId = this.onSignature(
rawSignature,
(result: SignatureResult, context: Context) => {
subscriptionId = undefined;
signatureSubscriptionId = undefined;
const response = {
context,
value: result,
Expand All @@ -3407,48 +3410,48 @@ export class Connection {
},
subscriptionCommitment,
);

if (!done) {
const args = this._buildArgs(
[rawSignature],
commitment || this._commitment || 'finalized', // Apply connection/server default.
);
const hash = fastStableStringify(
['signatureSubscribe', args],
true /* isArrayProp */,
);
intervalId = setInterval(() => {
const subscription = this._subscriptionsByHash[hash];
if (subscription && subscription.state === 'subscribed') {
(async () => {
const signatureStatuses = await this.getSignatureStatuses([
rawSignature,
]);
const result = signatureStatuses && signatureStatuses.value[0];
if (result?.err) {
reject(result.err);
}
if (result) {
const response = {
context: signatureStatuses.context,
value: result,
};
done = true;
resolve({__type: TransactionStatus.PROCESSED, response});
}
if (intervalId) {
clearInterval(intervalId);
}
})();
const subscriptionSetupPromise = new Promise<void>(
resolveSubscriptionSetup => {
if (signatureSubscriptionId == null) {
resolveSubscriptionSetup();
} else {
disposeSignatureSubscriptionStateChangeObserver =
this._onSubscriptionStateChange(
signatureSubscriptionId,
nextState => {
if (nextState === 'subscribed') {
resolveSubscriptionSetup();
}
},
);
}
}, 100);
}
},
);
(async () => {
await subscriptionSetupPromise;
if (done) return;
const response = await this.getSignatureStatus(rawSignature);
if (done) return;
if (response == null) {
return;
}
const {context, value} = response;
if (value?.err) {
reject(value.err);
}
if (value) {
done = true;
resolve({
__type: TransactionStatus.PROCESSED,
response: {
context,
value,
},
});
}
})();
} catch (err) {
reject(err);
} finally {
if (intervalId) {
clearInterval(intervalId);
}
}
});

Expand Down Expand Up @@ -3519,8 +3522,11 @@ export class Connection {
}
} finally {
clearTimeout(timeoutId);
if (subscriptionId) {
this.removeSignatureListener(subscriptionId);
if (disposeSignatureSubscriptionStateChangeObserver) {
disposeSignatureSubscriptionStateChangeObserver();
}
if (signatureSubscriptionId) {
this.removeSignatureListener(signatureSubscriptionId);
}
}
return result;
Expand Down Expand Up @@ -5192,7 +5198,7 @@ export class Connection {

/**
* @internal
*/ // @ts-ignore
*/
private _onSubscriptionStateChange(
clientSubscriptionId: ClientSubscriptionId,
callback: SubscriptionStateChangeCallback,
Expand Down
147 changes: 82 additions & 65 deletions web3.js/test/connection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {Buffer} from 'buffer';
import * as splToken from '@solana/spl-token';
import {expect, use} from 'chai';
import chaiAsPromised from 'chai-as-promised';
import {useFakeTimers, SinonFakeTimers} from 'sinon';
import {mock, useFakeTimers, SinonFakeTimers} from 'sinon';
import sinonChai from 'sinon-chai';

import {
Authorized,
Expand Down Expand Up @@ -67,6 +68,7 @@ import {MessageV0} from '../src/message/v0';
import {encodeData} from '../src/instruction';

use(chaiAsPromised);
use(sinonChai);

const verifySignatureStatus = (
status: SignatureStatus | null,
Expand Down Expand Up @@ -964,25 +966,6 @@ describe('Connection', function () {
expect(result.value).to.have.property('err', null);
}).timeout(60 * 1000);

it('confirms transactions that was already confirmed before', async () => {
let result = await connection.confirmTransaction(
{
signature,
...latestBlockhash,
},
'processed',
);
let result2 = await connection.confirmTransaction(
{
signature,
...latestBlockhash,
},
'processed',
);
expect(result.value).to.have.property('err', null);
expect(result2.value).to.have.property('err', null);
}).timeout(60 * 1000);

it('throws when confirming using a blockhash whose last valid blockheight has passed', async () => {
const confirmationPromise = connection.confirmTransaction({
signature,
Expand Down Expand Up @@ -1017,13 +1000,6 @@ describe('Connection', function () {
params: [mockSignature, {commitment: 'finalized'}],
result: new Promise(() => {}),
});

await mockRpcResponse({
method: 'getSignatureStatuses',
params: [[mockSignature]],
value: [null],
withContext: true,
});
const timeoutPromise = connection.confirmTransaction(mockSignature);

// Advance the clock past all waiting timers, notably the expiry timer.
Expand All @@ -1034,37 +1010,6 @@ describe('Connection', function () {
);
});

it('confirm transaction - by signature status', async () => {
const mockSignature =
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt';

await mockRpcMessage({
method: 'signatureSubscribe',
params: [mockSignature, {commitment: 'finalized'}],
result: new Promise(() => {}),
});

await mockRpcResponse({
method: 'getSignatureStatuses',
params: [[mockSignature]],
value: [
{
slot: 0,
confirmations: 11,
status: {Ok: null},
err: null,
},
],
withContext: true,
});
const promise = connection.confirmTransaction(mockSignature);

// Advance the clock past all waiting timers, notably the expiry timer.
clock.runAllAsync();

await expect(promise).not.to.be.rejected;
});

it('confirm transaction - block height exceeded', async () => {
const mockSignature =
'4oCEqwGrMdBeMxpzuWiukCYqSfV4DsSKXSiVVCh1iJ6pS772X7y219JZP3mgqBz5PhsvprpKyhzChjYc3VSBQXzG';
Expand All @@ -1075,13 +1020,6 @@ describe('Connection', function () {
result: new Promise(() => {}), // Never resolve this = never get a response.
});

await mockRpcResponse({
method: 'getSignatureStatuses',
params: [[mockSignature]],
value: [null],
withContext: true,
});

const lastValidBlockHeight = 3;

// Start the block height at `lastValidBlockHeight - 1`.
Expand Down Expand Up @@ -1197,6 +1135,85 @@ describe('Connection', function () {
value: {err: null},
});
});

it('confirm transaction - does not check the signature status before the signature subscription comes alive', async () => {
const mockSignature =
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt';

await mockRpcMessage({
method: 'signatureSubscribe',
params: [mockSignature, {commitment: 'finalized'}],
result: {err: null},
subscriptionEstablishmentPromise: new Promise(() => {}), // Never resolve.
});
const getSignatureStatusesExpectation = mock(connection)
.expects('getSignatureStatuses')
.never();
connection.confirmTransaction(mockSignature);
getSignatureStatusesExpectation.verify();
});

it('confirm transaction - checks the signature status once the signature subscription comes alive', async () => {
const mockSignature =
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt';

await mockRpcMessage({
method: 'signatureSubscribe',
params: [mockSignature, {commitment: 'finalized'}],
result: {err: null},
});
const getSignatureStatusesExpectation = mock(connection)
.expects('getSignatureStatuses')
.once();

const confirmationPromise =
connection.confirmTransaction(mockSignature);
clock.runAllAsync();

await expect(confirmationPromise).to.eventually.deep.equal({
context: {slot: 11},
value: {err: null},
});
getSignatureStatusesExpectation.verify();
});

// FIXME: This test does not work.
// it('confirm transaction - confirms transaction when signature status check yields confirmation before signature subscription does', async () => {
// const mockSignature =
// 'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt';

// // Keep the subscription from ever returning data.
// await mockRpcMessage({
// method: 'signatureSubscribe',
// params: [mockSignature, {commitment: 'finalized'}],
// result: new Promise(() => {}), // Never resolve.
// });
// clock.runAllAsync();

// const confirmationPromise =
// connection.confirmTransaction(mockSignature);
// clock.runAllAsync();

// // Return a signature status through the RPC API.
// await mockRpcResponse({
// method: 'getSignatureStatuses',
// params: [[mockSignature]],
// value: [
// {
// slot: 0,
// confirmations: 11,
// status: {Ok: null},
// err: null,
// },
// ],
// });
// clock.runAllAsync();

// await expect(confirmationPromise).to.eventually.deep.equal({
// context: {slot: 11},
// value: {err: null},
// });
// });
});
}

Expand Down

0 comments on commit 78b36ee

Please sign in to comment.