Skip to content

Commit

Permalink
feat(wallet): add UTxO repository and in-memory implementation
Browse files Browse the repository at this point in the history
- Adds a stub provider
  • Loading branch information
rhyslbw committed Sep 27, 2021
1 parent 07b9eb9 commit 1dc98c3
Show file tree
Hide file tree
Showing 11 changed files with 316 additions and 4 deletions.
10 changes: 7 additions & 3 deletions packages/wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@
"tscNoEmit": "shx echo typescript --noEmit command not implemented yet",
"cleanup": "shx rm -rf dist node_modules",
"lint": "eslint --ignore-path ../../.eslintignore \"**/*.ts\"",
"test": "shx echo No tests in this package",
"test": "jest -c ./test/jest.config.js",
"test:debug": "DEBUG=true yarn test"
},
"devDependencies": {
"@emurgo/cardano-serialization-lib-nodejs": "^8.0.0",
"@cardano-sdk/in-memory-key-manager": "0.1.0",
"shx": "^0.3.3"
},
"dependencies": {
"@cardano-ogmios/schema": "^4.0.0-beta.6",
"@cardano-sdk/cardano-serialization-lib": "0.1.0"
"@cardano-sdk/cardano-serialization-lib": "0.1.0",
"@cardano-sdk/cip2": "0.1.0",
"@cardano-sdk/core": "0.1.0",
"@emurgo/cardano-serialization-lib-nodejs": "^8.0.0",
"ts-log": "^2.2.3"
}
}
75 changes: 75 additions & 0 deletions packages/wallet/src/InMemoryUtxoRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import Schema, { TxIn, TxOut } from '@cardano-ogmios/schema';
import { UtxoRepository } from './UtxoRepository';
import { CardanoProvider, Ogmios } from '@cardano-sdk/core';
import { dummyLogger, Logger } from 'ts-log';
import { InputSelector, SelectionConstraints, SelectionResult } from '@cardano-sdk/cip2';
import CardanoSerializationLib from '@emurgo/cardano-serialization-lib-nodejs';
import { KeyManager } from './KeyManagement';

export class InMemoryUtxoRepository implements UtxoRepository {
#delegationAndRewards: Schema.DelegationsAndRewards;
#inputSelector: InputSelector;
#keyManager: KeyManager;
#logger: Logger;
#provider: CardanoProvider;
#utxoSet: Set<[TxIn, TxOut]>;

constructor(provider: CardanoProvider, keyManager: KeyManager, inputSelector: InputSelector, logger?: Logger) {
this.#logger = logger ?? dummyLogger;
this.#provider = provider;
this.#utxoSet = new Set();
this.#delegationAndRewards = { rewards: null, delegate: null };
this.#inputSelector = inputSelector;
this.#keyManager = keyManager;
}

public async sync(): Promise<void> {
this.#logger.debug('Syncing InMemoryUtxoRepository');
const result = await this.#provider.utxoDelegationAndRewards(
[this.#keyManager.deriveAddress(1, 0)],
Buffer.from(this.#keyManager.stakeKey.hash().to_bytes()).toString('hex')
);
this.#logger.trace(result);
for (const utxo of result.utxo) {
if (!this.#utxoSet.has(utxo)) {
this.#utxoSet.add(utxo);
this.#logger.debug('New UTxO', utxo);
}
}
if (this.#delegationAndRewards.delegate !== result.delegationAndRewards.delegate) {
this.#delegationAndRewards.delegate = result.delegationAndRewards.delegate;
this.#logger.debug('Delegation stored', result.delegationAndRewards.delegate);
}
if (this.#delegationAndRewards.rewards !== result.delegationAndRewards.rewards) {
this.#delegationAndRewards.rewards = result.delegationAndRewards.rewards;
this.#logger.debug('Rewards balance stored', result.delegationAndRewards.rewards);
}
}

public async selectInputs(
outputs: CardanoSerializationLib.TransactionOutputs,
constraints: SelectionConstraints
): Promise<SelectionResult> {
if (this.#utxoSet.size === 0) {
this.#logger.debug('Local UTxO set is empty. Syncing...');
await this.sync();
}
return this.#inputSelector.select({
utxo: Ogmios.OgmiosToCardanoWasm.utxo([...this.#utxoSet.values()]),
outputs,
constraints
});
}

public get allUtxos(): Schema.Utxo {
return [...this.#utxoSet.values()];
}

public get rewards(): Schema.Lovelace {
return this.#delegationAndRewards.rewards;
}

public get delegation(): Schema.PoolId {
return this.#delegationAndRewards.delegate;
}
}
14 changes: 14 additions & 0 deletions packages/wallet/src/UtxoRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Schema from '@cardano-ogmios/schema';
import { SelectionConstraints, SelectionResult } from '@cardano-sdk/cip2';
import CardanoSerializationLib from '@emurgo/cardano-serialization-lib-nodejs';

export interface UtxoRepository {
allUtxos: Schema.Utxo;
rewards: Schema.Lovelace;
delegation: Schema.PoolId;
sync: () => Promise<void>;
selectInputs: (
outputs: CardanoSerializationLib.TransactionOutputs,
constraints: SelectionConstraints
) => Promise<SelectionResult>;
}
2 changes: 2 additions & 0 deletions packages/wallet/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * as Address from './Address';
export * from './InMemoryUtxoRepository';
export * as KeyManagement from './KeyManagement';
export * from './UtxoRepository';
1 change: 1 addition & 0 deletions packages/wallet/src/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
},
"references": [
{ "path": "../../cardano-serialization-lib/src" },
{ "path": "../../cip2/src" },
{ "path": "../../core/src" }
]
}
80 changes: 80 additions & 0 deletions packages/wallet/test/InMemoryUtxoRepository.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { CardanoProvider, Ogmios } from '@cardano-sdk/core';
import { UtxoRepository } from '@src/UtxoRepository';
import { InMemoryUtxoRepository } from '@src/InMemoryUtxoRepository';
import { roundRobinRandomImprove, InputSelector } from '@cardano-sdk/cip2';
import CardanoSerializationLib from '@emurgo/cardano-serialization-lib-nodejs';
import { loadCardanoSerializationLib } from '@cardano-sdk/cardano-serialization-lib';
import { providerStub, delegate, rewards } from './ProviderStub';
import { createInMemoryKeyManager, util } from '@cardano-sdk/in-memory-key-manager';
import { NO_CONSTRAINTS } from './util';

const addresses = [
'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g'
];
// const stakeKeyHash = 'stake_test1up7pvfq8zn4quy45r2g572290p9vf99mr9tn7r9xrgy2l2qdsf58d';

// const txIn: OgmiosSchema.TxIn[] = [
// { txId: '0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5', index: 0 },
// { txId: '08fc1f8af3abbc8fc1f8a466e6e754833a08a62a059bf059bf5483a8a66e6e74', index: 0 }
// ];

const outputs = CardanoSerializationLib.TransactionOutputs.new();

outputs.add(
Ogmios.OgmiosToCardanoWasm.txOut({
address: addresses[0],
// value: { coins: 4_000_000, assets: { '2a286ad895d091f2b3d168a6091ad2627d30a72761a5bc36eef00740': 20n } }
value: { coins: 4_000_000 }
})
);
outputs.add(
Ogmios.OgmiosToCardanoWasm.txOut({
address: addresses[0],
// value: { coins: 2_000_000, assets: { '2a286ad895d091f2b3d168a6091ad2627d30a72761a5bc36eef00740': 20n } }
value: { coins: 2_000_000 }
})
);

describe('InMemoryUtxoRepository', () => {
let utxoRepository: UtxoRepository;
let provider: CardanoProvider;
let inputSelector: InputSelector;
let cardanoSerializationLib: typeof CardanoSerializationLib;

beforeEach(async () => {
provider = providerStub();
cardanoSerializationLib = await loadCardanoSerializationLib();
inputSelector = roundRobinRandomImprove(cardanoSerializationLib);
const keyManager = createInMemoryKeyManager({
mnemonic: util.generateMnemonic(),
networkId: 0,
password: '123'
});
utxoRepository = new InMemoryUtxoRepository(provider, keyManager, inputSelector);
});

test('constructed state', async () => {
await expect(utxoRepository.allUtxos.length).toBe(0);
await expect(utxoRepository.rewards).toBe(null);
await expect(utxoRepository.delegation).toBe(null);
});

test('sync', async () => {
await utxoRepository.sync();
await expect(utxoRepository.allUtxos.length).toBe(3);
await expect(utxoRepository.rewards).toBe(rewards);
await expect(utxoRepository.delegation).toBe(delegate);
});

describe('selectInputs', () => {
it('can be called without explicitly syncing', async () => {
const result = await utxoRepository.selectInputs(outputs, NO_CONSTRAINTS);
await expect(utxoRepository.allUtxos.length).toBe(3);
await expect(utxoRepository.rewards).toBe(rewards);
await expect(utxoRepository.delegation).toBe(delegate);
await expect(result.selection.inputs.length).toBeGreaterThan(0);
await expect(result.selection.outputs).toBe(outputs);
await expect(result.selection.change.length).toBe(2);
});
});
});
120 changes: 120 additions & 0 deletions packages/wallet/test/ProviderStub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/* eslint-disable max-len */
import { CardanoProvider } from '@cardano-sdk/core';
import * as Schema from '@cardano-ogmios/schema';

export const stakeKeyHash = 'stake_test1up7pvfq8zn4quy45r2g572290p9vf99mr9tn7r9xrgy2l2qdsf58d';

export const utxo: Schema.Utxo = [
[
{
txId: 'bb217abaca60fc0ca68c1555eca6a96d2478547818ae76ce6836133f3cc546e0',
index: 1
},
{
address:
'addr_test1qzs0umu0s2ammmpw0hea0w2crtcymdjvvlqngpgqy76gpfnuzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475qp3y3vz',
value: {
coins: 4_027_026_465
},
datum: null
}
],
[
{
txId: 'c7c0973c6bbf1a04a9f306da7814b4fa564db649bf48b0bd93c273bd03143547',
index: 0
},
{
address:
'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g',
value: {
coins: 3_289_566
},
datum: null
}
],
[
{
txId: 'ea1517b8c36fea3148df9aa1f49bbee66ff59a5092331a67bd8b3c427e1d79d7',
index: 2
},
{
address:
'addr_test1qqydn46r6mhge0kfpqmt36m6q43knzsd9ga32n96m89px3nuzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475qypp3m9',
value: {
coins: 9_825_963
},
datum: null
}
]
];

export const delegate = 'pool185g59xpqzt7gf0ljr8v8f3akl95qnmardf2f8auwr3ffx7atjj5';
export const rewards = 33_333;

/**
* Provider stub for testing
*
* @returns {CardanoProvider} CardanoProvider
*/

export const providerStub = (): CardanoProvider => ({
ledgerTip: async () => ({
blockNo: 1_111_111,
hash: '10d64cc11e9b20e15b6c46aa7b1fed11246f437e62225655a30ea47bf8cc22d0',
slot: 37_834_496
}),
submitTx: async () => true,
utxoDelegationAndRewards: async () => {
const delegationAndRewards = {
delegate,
rewards
};

return { utxo, delegationAndRewards };
},
queryTransactionsByAddresses: async () =>
Promise.resolve([
{
hash: 'ea1517b8c36fea3148df9aa1f49bbee66ff59a5092331a67bd8b3c427e1d79d7',
inputs: [
{
txId: 'bb217abaca60fc0ca68c1555eca6a96d2478547818ae76ce6836133f3cc546e0',
index: 0
}
],
outputs: [
{
address:
'addr_test1qpfhhfy2qgls50r9u4yh0l7z67xpg0a5rrhkmvzcuqrd0znuzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q9gw0lz',
value: { coins: 5_000_000 }
},
{
address:
'addr_test1qplfzem2xsc29wxysf8wkdqrm4s4mmncd40qnjq9sk84l3tuzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q52ukj5',
value: { coins: 5_000_000 }
},
{
address:
'addr_test1qqydn46r6mhge0kfpqmt36m6q43knzsd9ga32n96m89px3nuzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475qypp3m9',
value: { coins: 9_825_963 }
}
]
}
]),
queryTransactionsByHashes: async () => {
throw new Error('Not implemented yet');
},
currentWalletProtocolParameters: async () => ({
minFeeCoefficient: 44,
minFeeConstant: 155_381,
stakeKeyDeposit: 2_000_000,
poolDeposit: 500_000_000,
protocolVersion: { major: 5, minor: 0 },
minPoolCost: 340_000_000,
maxTxSize: 16_384,
maxValueSize: 1000,
maxCollateralInputs: 1,
coinsPerUtxoWord: 34_482
})
});
1 change: 1 addition & 0 deletions packages/wallet/test/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const { pathsToModuleNameMapper } = require('ts-jest/utils')
const { compilerOptions } = require('./tsconfig')

module.exports = {
setupFilesAfterEnv: ['./jest.setup.js'],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths),
preset: 'ts-jest',
transform: {
Expand Down
4 changes: 4 additions & 0 deletions packages/wallet/test/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// TODO: jest environment is not happy with 'lodash-es' exports.
// I think using non-es-module 'lodash' in 'dependencies' is too heavy.
// eslint-disable-next-line unicorn/prefer-module
jest.mock('lodash-es', () => require('lodash'));
5 changes: 4 additions & 1 deletion packages/wallet/test/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
}
},
"references": [
{ "path": "../src" }
{ "path": "../src" },
{ "path": "../../cip2/src" },
{ "path": "../../core/src" },
{ "path": "../../in-memory-key-manager/src" }
]
}
8 changes: 8 additions & 0 deletions packages/wallet/test/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SelectionConstraints } from '@cardano-sdk/cip2';

export const NO_CONSTRAINTS: SelectionConstraints = {
computeMinimumCoinQuantity: () => 1_000_000n,
computeMinimumCost: async () => 170_000n,
computeSelectionLimit: async () => 5,
tokenBundleSizeExceedsLimit: () => false
};

0 comments on commit 1dc98c3

Please sign in to comment.