Skip to content

feat(sdk): re-fetch nonce on interval #1706

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 22, 2024
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Identifier } from '@dashevo/wasm-dpp';
import { expect } from 'chai';
import NonceManager, { NONCE_FETCH_INTERVAL } from './NonceManager';

describe('Dash - NonceManager', () => {
let nonceManager: NonceManager;
let dapiClientMock;
const identityId = new Identifier(Buffer.alloc(32).fill(1));
const contractId = new Identifier(Buffer.alloc(32).fill(2));

beforeEach(function beforeEach() {
dapiClientMock = {
platform: {
getIdentityContractNonce: this.sinon.stub(),
getIdentityNonce: this.sinon.stub(),
},
};

nonceManager = new NonceManager(dapiClientMock);
});

describe('Identity nonce', () => {
it('should set and get identity nonce', async () => {
nonceManager.setIdentityNonce(identityId, 1);
expect(await nonceManager.getIdentityNonce(identityId)).to.be.equal(1);
expect(dapiClientMock.platform.getIdentityNonce).to.not.be.called();
});

it('should fetch identity nonce if it is not present', async () => {
dapiClientMock.platform.getIdentityNonce.resolves({ identityNonce: 1 });
expect(await nonceManager.getIdentityNonce(identityId)).to.be.equal(1);
expect(dapiClientMock.platform.getIdentityNonce).to.be.calledOnce();
});

it('should invalidate and re-fetch nonce after interval passed', async function it() {
const clock = this.sinon.useFakeTimers();
dapiClientMock.platform.getIdentityNonce.resolves({ identityNonce: 1 });
expect(await nonceManager.getIdentityNonce(identityId)).to.be.equal(1);

clock.tick(NONCE_FETCH_INTERVAL + 1);
dapiClientMock.platform.getIdentityNonce.resolves({ identityNonce: 2 });
await nonceManager.getIdentityNonce(identityId);
expect(await nonceManager.getIdentityNonce(identityId)).to.be.equal(2);
clock.restore();
});
});

describe('Identity contract nonce', () => {
it('should set and get identity contract nonce', async () => {
nonceManager.setIdentityContractNonce(identityId, contractId, 1);
expect(await nonceManager.getIdentityContractNonce(identityId, contractId))
.to.be.equal(1);
expect(dapiClientMock.platform.getIdentityContractNonce).to.not.be.called();
});

it('should fetch identity contract nonce if it is not present', async () => {
dapiClientMock.platform.getIdentityContractNonce.resolves({ identityContractNonce: 1 });
expect(await nonceManager.getIdentityContractNonce(identityId, contractId))
.to.be.equal(1);
expect(dapiClientMock.platform.getIdentityContractNonce).to.be.calledOnce();
});

it('should invalidate and re-fetch nonce after interval passed', async function it() {
const clock = this.sinon.useFakeTimers();
dapiClientMock.platform.getIdentityContractNonce.resolves({ identityContractNonce: 1 });
expect(await nonceManager.getIdentityContractNonce(identityId, contractId))
.to.be.equal(1);

clock.tick(NONCE_FETCH_INTERVAL + 1);
dapiClientMock.platform.getIdentityContractNonce.resolves({ identityContractNonce: 2 });
await nonceManager.getIdentityContractNonce(identityId, contractId);
expect(await nonceManager.getIdentityContractNonce(identityId, contractId))
.to.be.equal(2);
clock.restore();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import DAPIClient from '@dashevo/dapi-client';
import { Identifier } from '@dashevo/wasm-dpp';

// TODO: re-fetch nonces by timeout
type NonceState = {
value: number,
lastFetchedAt: number,
};

// 20 min
export const NONCE_FETCH_INTERVAL = 1200 * 1000;

class NonceManager {
public dapiClient: DAPIClient;

private identityNonce: Map<Identifier, number>;
private identityNonce: Map<Identifier, NonceState>;

private identityContractNonce: Map<Identifier, Map<Identifier, number>>;
private identityContractNonce: Map<Identifier, Map<Identifier, NonceState>>;

constructor(dapiClient: DAPIClient) {
this.dapiClient = dapiClient;
Expand All @@ -17,23 +24,49 @@ class NonceManager {
}

public setIdentityNonce(identityId: Identifier, nonce: number) {
this.identityNonce.set(identityId, nonce);
const nonceState = this.identityNonce.get(identityId);

if (!nonceState) {
this.identityNonce.set(identityId, {
value: nonce,
lastFetchedAt: Date.now(),
});
} else {
nonceState.value = nonce;
}
}

public async getIdentityNonce(identityId: Identifier): Promise<number> {
let nonce = this.identityNonce.get(identityId);
let nonceState = this.identityNonce.get(identityId);

if (typeof nonce === 'undefined') {
({ identityNonce: nonce } = await this.dapiClient.platform.getIdentityNonce(identityId));
if (typeof nonceState === 'undefined') {
const { identityNonce } = await this.dapiClient.platform.getIdentityNonce(identityId);

if (typeof nonce === 'undefined') {
if (typeof identityNonce === 'undefined') {
throw new Error('Identity nonce is not found');
}

this.identityNonce.set(identityId, nonce);
nonceState = {
value: identityNonce,
lastFetchedAt: Date.now(),
};

this.identityNonce.set(identityId, nonceState);
} else {
const now = Date.now();
if (now - nonceState.lastFetchedAt > NONCE_FETCH_INTERVAL) {
const { identityNonce } = await this.dapiClient.platform.getIdentityNonce(identityId);

if (typeof identityNonce === 'undefined') {
throw new Error('Identity nonce is not found');
}

nonceState.value = identityNonce;
nonceState.lastFetchedAt = now;
}
}

return nonce;
return nonceState.value;
}

public setIdentityContractNonce(identityId: Identifier, contractId: Identifier, nonce: number) {
Expand All @@ -44,7 +77,16 @@ class NonceManager {
this.identityContractNonce.set(identityId, contractNonce);
}

contractNonce.set(contractId, nonce);
const nonceState = contractNonce.get(contractId);

if (!nonceState) {
contractNonce.set(contractId, {
value: nonce,
lastFetchedAt: Date.now(),
});
} else {
nonceState.value = nonce;
}
}

public async getIdentityContractNonce(
Expand All @@ -58,20 +100,38 @@ class NonceManager {
this.identityContractNonce.set(identityId, contractNonce);
}

let nonce = contractNonce.get(contractId);
let nonceState = contractNonce.get(contractId);

if (typeof nonce === 'undefined') {
({ identityContractNonce: nonce } = await this.dapiClient.platform
.getIdentityContractNonce(identityId, contractId));
if (typeof nonceState === 'undefined') {
const { identityContractNonce } = await this.dapiClient.platform
.getIdentityContractNonce(identityId, contractId);

if (typeof nonce === 'undefined') {
if (typeof identityContractNonce === 'undefined') {
throw new Error('Identity contract nonce is not found');
}

contractNonce.set(identityId, nonce);
nonceState = {
value: identityContractNonce,
lastFetchedAt: Date.now(),
};

contractNonce.set(identityId, nonceState);
} else {
const now = Date.now();
if (now - nonceState.lastFetchedAt > NONCE_FETCH_INTERVAL) {
const { identityNonceContract } = await this.dapiClient.platform
.getIdentityContractNonce(identityId, contractId);

if (typeof identityNonceContract === 'undefined') {
throw new Error('Identity nonce is not found');
}

nonceState.value = identityNonceContract;
nonceState.lastFetchedAt = now;
}
}

return nonce;
return nonceState.value;
}
}

Expand Down
27 changes: 0 additions & 27 deletions packages/platform-test-suite/test/e2e/withdrawals.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,33 +146,6 @@ describe('Withdrawals', function withdrawalsTest() {
},
);

const identityId = identity.getId();
const dataContractId = client.getApps().get('withdrawals').contractId;
const { identityContractNonce } = await client.getDAPIClient().platform
.getIdentityContractNonce(identityId, dataContractId);

const stateTransition = client.platform.dpp.document.createStateTransition({
create: [withdrawal],
}, {
[identityId.toString()]: {
[dataContractId.toString()]: identityContractNonce,
},
});

stateTransition.setSignaturePublicKeyId(1);

const account = await client.getWalletAccount();

const { privateKey } = account.identities.getIdentityHDKeyById(
identity.getId().toString(),
1,
);

await stateTransition.sign(
identity.getPublicKeyById(1),
privateKey.toBuffer(),
);

try {
await client.platform.documents.broadcast({
create: [withdrawal],
Expand Down