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 SingleAddressWallet
This implementation uses hard-coded return values for the selection constraints, to be replaced by a later scope of work.
- Loading branch information
Showing
3 changed files
with
182 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import Schema from '@cardano-ogmios/schema'; | ||
import CardanoSerializationLib from '@emurgo/cardano-serialization-lib-nodejs'; | ||
import { CardanoProvider, Ogmios, Transaction } from '@cardano-sdk/core'; | ||
import { createTransactionInternals, KeyManagement, TxInternals, UtxoRepository } from './'; | ||
import { dummyLogger, Logger } from 'ts-log'; | ||
|
||
export type InitializeTxProps = { | ||
outputs: Schema.TxOut[]; | ||
options?: { | ||
validityInterval?: Transaction.ValidityInterval; | ||
}; | ||
}; | ||
|
||
export interface SingleAddressWallet { | ||
address: Schema.Address; | ||
initializeTx: (props: InitializeTxProps) => Promise<TxInternals>; | ||
signTx: ( | ||
body: CardanoSerializationLib.TransactionBody, | ||
hash: CardanoSerializationLib.TransactionHash | ||
) => Promise<CardanoSerializationLib.Transaction>; | ||
submitTx: (tx: CardanoSerializationLib.Transaction) => Promise<boolean>; | ||
} | ||
|
||
const ensureValidityInterval = ( | ||
currentSlot: number, | ||
validityInterval?: Transaction.ValidityInterval | ||
): Transaction.ValidityInterval => | ||
// Todo: Based this on slot duration, to equal 2hrs | ||
({ invalidHereafter: currentSlot + 3600, ...validityInterval }); | ||
|
||
export const createSingleAddressWallet = async ( | ||
CSL: typeof CardanoSerializationLib, | ||
provider: CardanoProvider, | ||
keyManager: KeyManagement.KeyManager, | ||
utxoRepository: UtxoRepository, | ||
logger: Logger = dummyLogger | ||
): Promise<SingleAddressWallet> => { | ||
const address = keyManager.deriveAddress(0, 0); | ||
const protocolParameters = await provider.currentWalletProtocolParameters(); | ||
return { | ||
address, | ||
initializeTx: async (props) => { | ||
const tip = await provider.ledgerTip(); | ||
const validityInterval = ensureValidityInterval(tip.slot, props.options?.validityInterval); | ||
const txOutputs = CSL.TransactionOutputs.new(); | ||
for (const output of props.outputs) { | ||
txOutputs.add(Ogmios.OgmiosToCardanoWasm.txOut(output)); | ||
} | ||
const inputSelectionResult = await utxoRepository.selectInputs(txOutputs, { | ||
computeMinimumCost: async ({ utxo, outputs, change }) => { | ||
const transactionInternals = await createTransactionInternals(CSL, { | ||
changeAddress: address, | ||
inputSelection: { | ||
outputs, | ||
inputs: utxo, | ||
change, | ||
fee: 0n | ||
}, | ||
validityInterval | ||
}); | ||
const witnessSet = await keyManager.signTransaction(transactionInternals.hash); | ||
const tx = CSL.Transaction.new(transactionInternals.body, witnessSet); | ||
return BigInt( | ||
CSL.min_fee( | ||
tx, | ||
CSL.LinearFee.new( | ||
CSL.BigNum.from_str(protocolParameters.minFeeCoefficient.toString()), | ||
CSL.BigNum.from_str(protocolParameters.minFeeConstant.toString()) | ||
) | ||
).to_str() | ||
); | ||
}, | ||
tokenBundleSizeExceedsLimit: (tokenBundle) => { | ||
logger.debug('SelectionConstraint: tokenBundleSizeExceedsLimit', tokenBundle); | ||
// Todo: Replace with real implementation | ||
return false; | ||
}, | ||
computeMinimumCoinQuantity: (assetQuantities) => { | ||
logger.debug('SelectionConstraint: computeMinimumCoinQuantity', assetQuantities); | ||
// Todo: Replace with real implementation | ||
return 1_000_000n; | ||
}, | ||
computeSelectionLimit: async (selectionSkeleton) => { | ||
logger.debug('SelectionConstraint: computeSelectionLimit', selectionSkeleton); | ||
// Todo: Replace with real implementation | ||
return 5; | ||
} | ||
}); | ||
return createTransactionInternals(CSL, { | ||
changeAddress: address, | ||
inputSelection: inputSelectionResult.selection, | ||
validityInterval | ||
}); | ||
}, | ||
signTx: async (body, hash) => { | ||
const witnessSet = await keyManager.signTransaction(hash); | ||
return CSL.Transaction.new(body, witnessSet); | ||
}, | ||
submitTx: async (tx) => provider.submitTx(tx) | ||
}; | ||
}; |
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 @@ | ||
/* eslint-disable max-len */ | ||
|
||
// All types should be imported from cardano-serialization-lib-nodejs. | ||
// Importing from cardano-serialization-lib-browser will cause TypeScript errors. | ||
// Do not create objects from direct imports of serialization lib, use @cardano-sdk/cardano-serialization-lib. | ||
|
||
import { createSingleAddressWallet, SingleAddressWallet } from '@src/SingleAddressWallet'; | ||
import * as KeyManagement from '@src/KeyManagement'; | ||
import { Cardano, CardanoProvider } from '@cardano-sdk/core'; | ||
import { createInMemoryKeyManager, util } from '@cardano-sdk/in-memory-key-manager'; | ||
import CardanoSerializationLib from '@emurgo/cardano-serialization-lib-nodejs'; | ||
import { loadCardanoSerializationLib } from '@cardano-sdk/cardano-serialization-lib'; | ||
import { InMemoryUtxoRepository } from '@src/InMemoryUtxoRepository'; | ||
import { UtxoRepository } from '@src/UtxoRepository'; | ||
import { InputSelector, roundRobinRandomImprove } from '@cardano-sdk/cip2'; | ||
import { providerStub } from './ProviderStub'; | ||
|
||
describe('Wallet', () => { | ||
let CSL: typeof CardanoSerializationLib; | ||
let inputSelector: InputSelector; | ||
let keyManager: KeyManagement.KeyManager; | ||
let provider: CardanoProvider; | ||
let utxoRepository: UtxoRepository; | ||
|
||
beforeEach(async () => { | ||
CSL = await loadCardanoSerializationLib(); | ||
keyManager = createInMemoryKeyManager({ | ||
mnemonic: util.generateMnemonic(), | ||
networkId: Cardano.NetworkId.testnet, | ||
password: '123' | ||
}); | ||
provider = providerStub(); | ||
inputSelector = roundRobinRandomImprove(CSL); | ||
utxoRepository = new InMemoryUtxoRepository(provider, keyManager, inputSelector); | ||
}); | ||
|
||
test('createWallet', async () => { | ||
const wallet = await createSingleAddressWallet(CSL, provider, keyManager, utxoRepository); | ||
expect(wallet.address).toBeDefined(); | ||
expect(typeof wallet.initializeTx).toBe('function'); | ||
expect(typeof wallet.signTx).toBe('function'); | ||
}); | ||
|
||
describe('wallet behaviour', () => { | ||
let wallet: SingleAddressWallet; | ||
const props = { | ||
outputs: [ | ||
{ | ||
address: | ||
'addr_test1qpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5ewvxwdrt70qlcpeeagscasafhffqsxy36t90ldv06wqrk2qum8x5w', | ||
value: { coins: 11_111_111 } | ||
} | ||
] | ||
}; | ||
|
||
beforeEach(async () => { | ||
wallet = await createSingleAddressWallet(CSL, provider, keyManager, utxoRepository); | ||
}); | ||
|
||
test('initializeTx', async () => { | ||
const txInternals = await wallet.initializeTx(props); | ||
expect(txInternals.body).toBeInstanceOf(CSL.TransactionBody); | ||
expect(txInternals.hash).toBeInstanceOf(CSL.TransactionHash); | ||
}); | ||
|
||
test('signTx', async () => { | ||
const { body, hash } = await wallet.initializeTx(props); | ||
const tx = await wallet.signTx(body, hash); | ||
await expect(tx.body().outputs().len()).toBe(1); | ||
await expect(tx.body().inputs().len()).toBeGreaterThan(0); | ||
}); | ||
|
||
test('submitTx', async () => { | ||
const { body, hash } = await wallet.initializeTx(props); | ||
const tx = await wallet.signTx(body, hash); | ||
const result = await wallet.submitTx(tx); | ||
expect(result).toBe(true); | ||
}); | ||
}); | ||
}); |