-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(wallet): add UTxO repository and in-memory implementation
- Adds a stub provider
- Loading branch information
Showing
11 changed files
with
316 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}) | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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')); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}; |