Skip to content

Commit

Permalink
feat(wallet): add SingleAddressWallet
Browse files Browse the repository at this point in the history
This implementation uses hard-coded return values for the selection constraints,
to be replaced by a later scope of work.
  • Loading branch information
rhyslbw committed Sep 26, 2021
1 parent 6c68449 commit fa4ae94
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 0 deletions.
101 changes: 101 additions & 0 deletions packages/wallet/src/SingleAddressWallet.ts
@@ -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)
};
};
1 change: 1 addition & 0 deletions packages/wallet/src/index.ts
Expand Up @@ -2,4 +2,5 @@ export * as Address from './Address';
export * from './createTransactionInternals';
export * from './InMemoryUtxoRepository';
export * as KeyManagement from './KeyManagement';
export * from './SingleAddressWallet';
export * from './UtxoRepository';
80 changes: 80 additions & 0 deletions packages/wallet/test/SingleAddressWallet.test.ts
@@ -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);
});
});
});

0 comments on commit fa4ae94

Please sign in to comment.