@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
263 lines • 11.8 kB
JavaScript
import { expect } from 'chai';
import { ContractFactory } from 'ethers';
import { Mailbox__factory, ProxyAdmin__factory, TestRecipient__factory, } from '@hyperlane-xyz/core';
import { Mailbox__factory as TronMailbox__factory, ProxyAdmin__factory as TronProxyAdmin__factory, TronContractFactory, TestRecipient__factory as TronTestRecipient__factory, } from '@hyperlane-xyz/tron-sdk';
import { TestChainName, test1, test2 } from '../consts/testChains.js';
import { EthJsonRpcBlockParameterTag } from '../metadata/chainMetadataTypes.js';
import sinon from 'sinon';
import { MultiProvider } from './MultiProvider.js';
describe('MultiProvider Tron factory resolution', () => {
const mp = new MultiProvider({});
it('resolves Mailbox to tron factory with different bytecode', async () => {
const resolved = await mp.resolveTronFactory(new Mailbox__factory());
expect(resolved.constructor.name).to.equal(TronContractFactory.name);
expect(resolved.bytecode).to.equal(new TronMailbox__factory().bytecode);
expect(resolved.bytecode).to.not.equal(new Mailbox__factory().bytecode);
});
it('resolves ProxyAdmin to tron factory', async () => {
const resolved = await mp.resolveTronFactory(new ProxyAdmin__factory());
expect(resolved.constructor.name).to.equal(TronContractFactory.name);
expect(resolved.bytecode).to.equal(new TronProxyAdmin__factory().bytecode);
});
it('resolves TestRecipient to tron factory', async () => {
const resolved = await mp.resolveTronFactory(new TestRecipient__factory());
expect(resolved.constructor.name).to.equal(TronContractFactory.name);
expect(resolved.bytecode).to.equal(new TronTestRecipient__factory().bytecode);
});
it('preserves ABI when resolving', async () => {
const resolved = await mp.resolveTronFactory(new Mailbox__factory());
expect(JSON.stringify(resolved.interface.fragments)).to.equal(JSON.stringify(new Mailbox__factory().interface.fragments));
});
it('throws for unknown factory', async () => {
class Unknown__factory extends ContractFactory {
constructor() {
super([], '0x');
}
}
try {
await mp.resolveTronFactory(new Unknown__factory());
expect.fail('Should have thrown');
}
catch (e) {
expect(e.message).to.include('No Tron-compiled factory found for');
}
});
});
describe('MultiProvider', () => {
describe('handleTx', () => {
let multiProvider;
beforeEach(() => {
const chainMetadata = {
[TestChainName.test1]: test1,
[TestChainName.test2]: test2,
};
multiProvider = new MultiProvider(chainMetadata);
});
it('should timeout when numeric confirmation never resolves', async () => {
const mockTx = {
hash: '0xabc123def456',
wait: sinon.stub().returns(new Promise(() => { })),
};
try {
await multiProvider.handleTx(TestChainName.test1, mockTx, {
timeoutMs: 100,
});
throw new Error('Expected timeout error');
}
catch (error) {
expect(error.message).to.include('Timeout');
expect(error.message).to.include('0xabc123def456');
expect(error.message).to.include('confirmations');
}
});
it('should return receipt when numeric confirmation resolves before timeout', async () => {
const mockReceipt = {
transactionHash: '0xabc123def456',
blockNumber: 100,
status: 1,
};
const mockTx = {
hash: '0xabc123def456',
wait: sinon.stub().resolves(mockReceipt),
};
const result = await multiProvider.handleTx(TestChainName.test1, mockTx, {
timeoutMs: 5000,
});
expect(result).to.deep.equal(mockReceipt);
expect(mockTx.wait.calledOnce).to.be.true;
});
it('should wait for inclusion when wait(0) returns null', async () => {
const mockReceipt = {
transactionHash: '0xabc123def456',
blockNumber: 100,
status: 1,
};
const waitStub = sinon
.stub()
.callsFake(async (confirmations) => {
if (confirmations === 0)
return null;
return mockReceipt;
});
const mockTx = {
hash: '0xabc123def456',
wait: waitStub,
};
const result = await multiProvider.handleTx(TestChainName.test1, mockTx, {
waitConfirmations: 0,
timeoutMs: 5000,
});
expect(result).to.deep.equal(mockReceipt);
expect(waitStub.calledTwice).to.be.true;
expect(waitStub.firstCall.args[0]).to.equal(0);
expect(waitStub.secondCall.args[0]).to.equal(1);
});
it('should not timeout when timeoutMs is 0', async () => {
const mockReceipt = {
transactionHash: '0xabc123def456',
blockNumber: 100,
status: 1,
};
const mockTx = {
hash: '0xabc123def456',
wait: sinon.stub().callsFake(() => new Promise((resolve) => {
setTimeout(() => resolve(mockReceipt), 50);
})),
};
const result = await multiProvider.handleTx(TestChainName.test1, mockTx, {
timeoutMs: 0,
});
expect(result).to.deep.equal(mockReceipt);
});
it('should apply default timeout when no options provided', async () => {
const mockTx = {
hash: '0xabc123def456',
wait: sinon.stub().returns(new Promise(() => { })),
};
try {
await Promise.race([
multiProvider.handleTx(TestChainName.test1, mockTx),
new Promise((_, reject) => setTimeout(() => reject(new Error('Test timeout')), 100)),
]);
throw new Error('Expected timeout error');
}
catch (error) {
expect(error.message).to.match(/Timeout|Test timeout/);
}
});
it('should apply minimum timeout floor for short estimateBlockTime', async () => {
const chainMetadataWithBlockTime = {
[TestChainName.test1]: {
...test1,
blocks: {
...test1.blocks,
confirmations: 1,
estimateBlockTime: 0.02,
},
},
[TestChainName.test2]: test2,
};
const mp = new MultiProvider(chainMetadataWithBlockTime);
const mockTx = {
hash: '0xabc123def456',
wait: sinon.stub().returns(new Promise(() => { })),
};
// Raw timeout: 1 × 0.02s × 1000 × 2 = 40ms
// With floor: max(40, 30000) = 30000ms
// Race against 200ms — if the floor works, 200ms timer wins (not a Timeout error)
try {
await Promise.race([
mp.handleTx(TestChainName.test1, mockTx),
new Promise((_, reject) => setTimeout(() => reject(new Error('Race timer')), 200)),
]);
throw new Error('Expected race timer');
}
catch (error) {
// Without the floor, handleTx would timeout at 40ms with "Timeout" error.
// With the floor, the 200ms race timer fires first.
expect(error.message).to.equal('Race timer');
}
});
it('should dispatch to waitForBlockTag for string confirmation', async () => {
const mockReceipt = {
transactionHash: '0xabc123def456',
blockNumber: 100,
status: 1,
};
const mockTx = {
hash: '0xabc123def456',
wait: sinon.stub().resolves(mockReceipt),
};
const waitForBlockTagStub = sinon
.stub(multiProvider, 'waitForBlockTag')
.resolves(mockReceipt);
const result = await multiProvider.handleTx(TestChainName.test1, mockTx, {
waitConfirmations: EthJsonRpcBlockParameterTag.Finalized,
});
expect(result).to.deep.equal(mockReceipt);
expect(waitForBlockTagStub.calledOnce).to.be.true;
waitForBlockTagStub.restore();
});
});
describe('tryGetSigner', () => {
it('should cache the connected signer for subsequent calls', () => {
const chainMetadata = {
[TestChainName.test1]: test1,
[TestChainName.test2]: test2,
};
const mp = new MultiProvider(chainMetadata);
let connectCallCount = 0;
const mockProvider = {};
const mockConnectedSigner = { provider: mockProvider };
const mockSigner = {
provider: undefined,
connect: sinon.stub().callsFake(() => {
connectCallCount += 1;
return mockConnectedSigner;
}),
};
mp.signers[TestChainName.test1] = mockSigner;
mp.providers[TestChainName.test1] = mockProvider;
// First call should connect and cache
const result1 = mp.tryGetSigner(TestChainName.test1);
expect(result1).to.equal(mockConnectedSigner);
expect(connectCallCount).to.equal(1);
// Second call should return cached signer without calling connect again
const result2 = mp.tryGetSigner(TestChainName.test1);
expect(result2).to.equal(mockConnectedSigner);
expect(connectCallCount).to.equal(1);
});
it('should not cache signer in shared-signer mode so provider swaps take effect', () => {
const chainMetadata = {
[TestChainName.test1]: test1,
[TestChainName.test2]: test2,
};
const mp = new MultiProvider(chainMetadata);
const oldProvider = {};
const newProvider = {};
let connectArg;
const mockSigner = {
provider: undefined,
connect: sinon.stub().callsFake((p) => {
connectArg = p;
return { provider: p, getAddress: () => '0x1' };
}),
};
// Use shared signer mode
mp.useSharedSigner = true;
mp.signers[TestChainName.test1] = mockSigner;
mp.providers[TestChainName.test1] = oldProvider;
// First call connects to old provider
const result1 = mp.tryGetSigner(TestChainName.test1);
expect(connectArg).to.equal(oldProvider);
expect(result1.provider).to.equal(oldProvider);
// Swap provider — in shared mode, setProvider skips reconnection
mp.providers[TestChainName.test1] = newProvider;
// Second call should reconnect to new provider (not return stale cached signer)
const result2 = mp.tryGetSigner(TestChainName.test1);
expect(connectArg).to.equal(newProvider);
expect(result2.provider).to.equal(newProvider);
});
});
});
//# sourceMappingURL=MultiProvider.test.js.map