Skip to content
Open
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
18 changes: 14 additions & 4 deletions packages/bitcore-node/src/providers/chain-state/svm/api/csp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ import type { SolRpc } from '@bitpay-labs/crypto-rpc/lib/sol/SolRpc';

export interface GetSolWeb3Response { rpc: SolRpc; connection: any; umi: any; dataType: string; lastPingTime?: number };

function isMissingAtaError(err: any): boolean {
const message = err?.message || '';
return message === 'Missing ATA' || message.includes('ATA not initialized');
}

export class BaseSVMStateProvider extends InternalStateProvider implements IChainStateService {
static rpcs: { [chainNetwork: string]: { historical: GetSolWeb3Response[]; realtime: GetSolWeb3Response[] } } = {};
static rpcIndicies: { [chainNetwork: string]: { historical: number; realtime: number } } = {};
Expand Down Expand Up @@ -285,13 +290,18 @@ export class BaseSVMStateProvider extends InternalStateProvider implements IChai
let _address = address;
if (tokenAddress) {
try {
const { rpc } = await this.getRpc(network);
_address = await rpc.getConfirmedAta({ solAddress: address, mintAddress: tokenAddress });
if (!_address) throw new Error('Missing ATA');
} catch (e: any) {
const errMsg = 'Error getting ATA address';
logger.error(`${errMsg} %o`, e.stack || e.message || e);
throw new Error(errMsg);
if (isMissingAtaError(e)) {
// A closed ATA is not returned by getConfirmedAta, but its address can still have history.
_address = await rpc.deriveAta({ solAddress: address, mintAddress: tokenAddress });
if (!_address) throw new Error('Missing ATA');
} else {
const errMsg = 'Error getting ATA address';
logger.error(`${errMsg} %o`, e.stack || e.message || e);
throw new Error(errMsg);
}
}
}
do {
Expand Down
52 changes: 52 additions & 0 deletions packages/bitcore-node/test/integration/solana/csp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,58 @@ describe('Solana API', function() {
expect(err).to.not.exist;
expect(counter).to.be.gt(0);
});

it('should stream empty SPL token history from derived ATA when current ATA is not initialized', async () => {
const tokenAddress = '11111111111111111111111111111112';
const derivedAtaAddress = 'derivedTokenAccountAddress';
const chunks: string[] = [];
const connection = {
getSignaturesForAddress: sandbox.stub().returns({ send: sandbox.stub().resolves([]) })
};
const rpc = {
getConfirmedAta: sandbox.stub().rejects(new Error('ATA not initialized on mint for provided account. Initialize ATA first.')),
deriveAta: sandbox.stub().resolves(derivedAtaAddress)
};

sandbox.stub(SOL, 'getRpc').resolves({ rpc, connection });
sandbox.stub(SOL, 'getWalletAddresses').resolves([{ address }]);

const req = (new Writable({
write: function(_data, _, cb) {
cb();
}
}) as unknown) as Request;

const res = (new Writable({
write: function(data, _, cb) {
chunks.push(data.toString());
cb();
}
}) as unknown) as Response;
res.type = () => res;

const err = await new Promise(r => {
res
.on('error', r)
.on('finish', () => r(undefined));

SOL.streamWalletTransactions({
chain,
network,
wallet,
req,
res,
args: { tokenAddress }
})
.catch(e => r(e));
});

expect(err).to.not.exist;
expect(chunks.join('')).to.equal('');
expect(rpc.deriveAta.calledOnceWithExactly({ solAddress: address, mintAddress: tokenAddress })).to.be.true;
expect(connection.getSignaturesForAddress.calledOnce).to.be.true;
expect(connection.getSignaturesForAddress.firstCall.args[0]).to.equal(derivedAtaAddress);
});
});

it('should correctly transform transaction data', () => {
Expand Down