Skip to content

Commit

Permalink
Cardano Primitives (#29)
Browse files Browse the repository at this point in the history
Add Cardano primitives interface to support multiple implementations. The Rust-derived cardano-wallet dependency is the default and has also been updated to recent tx balance behaviour
  • Loading branch information
Sam-Jeston authored and rhyslbw committed Jun 17, 2019
1 parent c528a4e commit f744f78
Show file tree
Hide file tree
Showing 47 changed files with 925 additions and 686 deletions.
4 changes: 2 additions & 2 deletions README.md
Expand Up @@ -29,8 +29,8 @@ The below examples are implemented as integration tests, they should be very eas

- [Generate a keypair in memory from a BIP39 mnemonic](src/test/MemoryKeyManager.spec.ts)
- [Message signatures](src/test/SignAndVerify.spec.ts)
- [Get the wallet balance for a BIP44 Public Account](src/test/WalletBalance.spec.ts)
- [Determine the next change and receipt addresses for a BIP44 Public Account](src/test/DetermineNextAddressForWallet.spec.ts)
- [Get the wallet balance for a BIP44 Account](src/test/WalletBalance.spec.ts)
- [Determine the next change and receipt addresses for a BIP44 Account](src/test/DetermineNextAddressForWallet.spec.ts)
- [Transaction input selection](src/test/SelectInputsForTransaction.spec.ts)

## Tests
Expand Down
49 changes: 49 additions & 0 deletions src/Cardano/Cardano.ts
@@ -0,0 +1,49 @@
import { TransactionInput, TransactionOutput } from '../Transaction'
import { FeeAlgorithm } from './FeeAlgorithm'
import { Transaction } from './Transaction'
import { AddressType, Address, UtxoWithAddressing } from '../Wallet'
import { ChainSettings } from './ChainSettings'
import { TransactionSelection } from './TransactionSelection'

export interface Cardano {
buildTransaction: (
inputs: TransactionInput[],
outputs: TransactionOutput[],
feeAlgorithm?: FeeAlgorithm
) => Transaction
account: (
mnemonic: string,
passphrase: string,
accountIndex: number
) => { privateParentKey: string, publicParentKey: string }
address: (
args: {
publicParentKey: string,
index: number,
type: AddressType
accountIndex: number
},
chainSettings?: ChainSettings
) => Address
signMessage: (
args: {
privateParentKey: string
addressType: AddressType
signingIndex: number
message: string
}
) => { signature: string, publicKey: string }
verifyMessage: (
args: {
publicKey: string
message: string
signature: string
}
) => boolean
inputSelection: (
outputs: TransactionOutput[],
utxoSet: UtxoWithAddressing[],
changeAddress: string,
feeAlgorithm?: FeeAlgorithm
) => TransactionSelection
}
3 changes: 3 additions & 0 deletions src/Cardano/ChainSettings.ts
@@ -0,0 +1,3 @@
export enum ChainSettings {
mainnet = 'mainnet',
}
3 changes: 3 additions & 0 deletions src/Cardano/FeeAlgorithm.ts
@@ -0,0 +1,3 @@
export enum FeeAlgorithm {
default = 'default'
}
14 changes: 14 additions & 0 deletions src/Cardano/Transaction.ts
@@ -0,0 +1,14 @@
import { TransactionInput } from '../Transaction'
import { AddressType } from '../Wallet'
import { ChainSettings } from './ChainSettings'

export interface Transaction {
toHex: () => string
toJson: () => any
id: () => string
addWitness: (args: { privateParentKey: string, addressing: TransactionInput['addressing'], chainSettings?: ChainSettings }) => void
addExternalWitness: (args: { publicParentKey: string, addressType: AddressType, witnessIndex: number, witnessHex: string }) => void
finalize: () => string
fee: () => string
estimateFee?: () => string
}
6 changes: 6 additions & 0 deletions src/Cardano/TransactionSelection.ts
@@ -0,0 +1,6 @@
import { TransactionInput, TransactionOutput } from '../Transaction'

export interface TransactionSelection {
inputs: TransactionInput[]
changeOutput: TransactionOutput
}
4 changes: 4 additions & 0 deletions src/Cardano/index.ts
@@ -0,0 +1,4 @@
export { Cardano } from './Cardano'
export { FeeAlgorithm } from './FeeAlgorithm'
export { ChainSettings } from './ChainSettings'
export { TransactionSelection } from './TransactionSelection'
44 changes: 0 additions & 44 deletions src/KeyManager/InMemoryKey/index.ts

This file was deleted.

6 changes: 3 additions & 3 deletions src/KeyManager/KeyManager.ts
@@ -1,9 +1,9 @@
import { BlockchainSettings as CardanoBlockchainSettings, Bip44AccountPublic } from 'cardano-wallet'
import { AddressType } from '../Wallet'
import Transaction, { TransactionInput } from '../Transaction'
import { ChainSettings } from '../Cardano'

export interface KeyManager {
signTransaction: (transaction: ReturnType<typeof Transaction>, inputs: TransactionInput[], chainSettings?: CardanoBlockchainSettings, transactionsAsProofForSpending?: { [transactionId: string]: string }) => Promise<string>
signTransaction: (transaction: ReturnType<typeof Transaction>, inputs: TransactionInput[], chainSettings?: ChainSettings, transactionsAsProofForSpending?: { [transactionId: string]: string }) => Promise<string>
signMessage: (addressType: AddressType, signingIndex: number, message: string) => Promise<{ publicKey: string, signature: string }>
publicAccount: () => Promise<Bip44AccountPublic>
publicParentKey: () => Promise<string>
}
3 changes: 2 additions & 1 deletion src/KeyManager/index.ts
@@ -1 +1,2 @@
export { InMemoryKeyManager } from './InMemoryKey'
export { KeyManager } from './KeyManager'
export * from './errors'
24 changes: 12 additions & 12 deletions src/Transaction/Transaction.spec.ts
Expand Up @@ -2,7 +2,7 @@ import { expect } from 'chai'
import Transaction, { TransactionInput, TransactionOutput } from './'
import { InsufficientTransactionInput } from './errors'
import { EmptyArray } from '../lib/validator/errors'
import { estimateTransactionFee } from '../Utils/estimate_fee'
import { RustCardano } from '../lib'

describe('Transaction', () => {
it('throws if inputs are invalid', () => {
Expand All @@ -13,8 +13,8 @@ describe('Transaction', () => {
{ address: 'Ae2tdPwUPEZCEhYAUVU7evPfQCJjyuwM6n81x6hSjU9TBMSy2YwZEVydssL', value: '10000' }
]

expect(() => Transaction(emptyInputArray, outputs)).to.throw(EmptyArray)
expect(() => Transaction(invalidInputType, outputs)).to.throw(/Invalid value/)
expect(() => Transaction(RustCardano, emptyInputArray, outputs)).to.throw(EmptyArray)
expect(() => Transaction(RustCardano, invalidInputType, outputs)).to.throw(/Invalid value/)
})

it('throws if outputs are invalid', () => {
Expand All @@ -26,8 +26,8 @@ describe('Transaction', () => {
const emptyOutputArray = [] as TransactionOutput[]
const invalidOutputType = [{ foo: 'bar' }] as any[]

expect(() => Transaction(inputs, emptyOutputArray)).to.throw(EmptyArray)
expect(() => Transaction(inputs, invalidOutputType)).to.throw(/Invalid value/)
expect(() => Transaction(RustCardano, inputs, emptyOutputArray)).to.throw(EmptyArray)
expect(() => Transaction(RustCardano, inputs, invalidOutputType)).to.throw(/Invalid value/)
})

it('throws if a transaction has more combined output value than input value', () => {
Expand All @@ -40,7 +40,7 @@ describe('Transaction', () => {
{ address: 'Ae2tdPwUPEZCEhYAUVU7evPfQCJjyuwM6n81x6hSjU9TBMSy2YwZEVydssL', value: '2000000' }
]

expect(() => Transaction(inputs, outputs)).to.throw(InsufficientTransactionInput)
expect(() => Transaction(RustCardano, inputs, outputs)).to.throw(InsufficientTransactionInput)
})

it('accepts more combined input value than output, to cover fees', () => {
Expand All @@ -53,23 +53,23 @@ describe('Transaction', () => {
{ address: 'Ae2tdPwUPEZCEhYAUVU7evPfQCJjyuwM6n81x6hSjU9TBMSy2YwZEVydssL', value: '10000' }
]

expect(() => Transaction(inputs, outputs)).to.not.throw()
expect(() => Transaction(RustCardano, inputs, outputs)).to.not.throw()
})

it('allows access to a transaction as hex', () => {
const inputs = [
{ pointer: { id: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', index: 1 }, value: { address: 'addressWithFunds1', value: '1000000' } },
{ pointer: { id: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', index: 1 }, value: { address: 'addressWithFunds1', value: '2000000' } },
{ pointer: { id: 'fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210', index: 0 }, value: { address: 'addressWithFunds2', value: '5000000' } }
]

let outputs = [
{ address: 'Ae2tdPwUPEZCEhYAUVU7evPfQCJjyuwM6n81x6hSjU9TBMSy2YwZEVydssL', value: '6000000' }
]

const fee = estimateTransactionFee(inputs, outputs)
const fee = Transaction(RustCardano, inputs, outputs).estimateFee()

outputs[0].value = (6000000 - Number(fee)).toString()
const transaction = Transaction(inputs, outputs)
const transaction = Transaction(RustCardano, inputs, outputs)
const expectedHex = '839f8200d81858248258200123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef018200d8185824825820fedcba9876543210fedcba9876543210fedcba9876543210fedcba987654321000ff9f8282d818582183581c9aa3c11f83717c117b5da7f49b9387dc90d1694a75849bd5cbde8e20a0001ae196744f1a0058e69dffa0'
expect(transaction.toHex()).to.equal(expectedHex)
})
Expand All @@ -85,8 +85,8 @@ describe('Transaction', () => {
{ address: 'Ae2tdPwUPEZCEhYAUVU7evPfQCJjyuwM6n81x6hSjU9TBMSy2YwZEVydssL', value: '5000000' }
]

const estimatedFee = estimateTransactionFee(inputs, outputs)
const realisedFee = Transaction(inputs, outputs).fee()
const estimatedFee = Transaction(RustCardano, inputs, outputs).estimateFee()
const realisedFee = Transaction(RustCardano, inputs, outputs).fee()

expect(realisedFee).to.equal('2010000')
expect(realisedFee).to.not.eql(estimatedFee)
Expand Down
51 changes: 3 additions & 48 deletions src/Transaction/Transaction.ts
@@ -1,55 +1,10 @@
import { getBindingsForEnvironment } from '../lib/bindings'
import { InsufficientTransactionInput } from './errors'
import { TransactionInput, TransactionInputCodec } from './TransactionInput'
import { TransactionOutput, TransactionOutputCodec } from './TransactionOutput'
import { validateCodec } from '../lib/validator'
import { convertCoinToLovelace } from '../Utils'
const { TransactionBuilder, TxoPointer, TxOut, Coin, LinearFeeAlgorithm, TransactionFinalized } = getBindingsForEnvironment()
import { FeeAlgorithm, Cardano } from '../Cardano'

export function Transaction (inputs: TransactionInput[], outputs: TransactionOutput[], feeAlgorithm = LinearFeeAlgorithm.default()) {
export function Transaction (cardano: Cardano, inputs: TransactionInput[], outputs: TransactionOutput[], feeAlgorithm = FeeAlgorithm.default) {
validateCodec<typeof TransactionInputCodec>(TransactionInputCodec, inputs)
validateCodec<typeof TransactionOutputCodec>(TransactionOutputCodec, outputs)

const transactionBuilder = buildTransaction(inputs, outputs)

const balance = transactionBuilder.get_balance(feeAlgorithm)
if (balance.is_negative()) throw new InsufficientTransactionInput()

/*
The get_balance_without_fees from the WASM bindings returns:
Σ(transactionInputValues) - Σ(transactionOutputValues)
This represents the fee paid on a transaction, as the positive balance
between inputs and the associated outputs is equal to the fee paid
*/
const feeAsCoinType = transactionBuilder.get_balance_without_fees().value()
const fee = convertCoinToLovelace(feeAsCoinType)

const cardanoTransaction = transactionBuilder.make_transaction()

return {
toHex: () => cardanoTransaction.to_hex(),
toJson: () => cardanoTransaction.to_json(),
id: () => cardanoTransaction.id(),
finalize: () => new TransactionFinalized(cardanoTransaction),
fee: () => fee
}
}

export function buildTransaction (inputs: TransactionInput[], outputs: TransactionOutput[]) {
const transactionBuilder = new TransactionBuilder()

inputs.forEach(input => {
const pointer = TxoPointer.from_json(input.pointer)
const value = Coin.from(0, Number(input.value.value))
transactionBuilder.add_input(pointer, value)
})

outputs.forEach(output => {
const txOut = TxOut.from_json(output)
transactionBuilder.add_output(txOut)
})

return transactionBuilder
return cardano.buildTransaction(inputs, outputs, feeAlgorithm)
}
3 changes: 2 additions & 1 deletion src/Transaction/TransactionInput.ts
Expand Up @@ -7,7 +7,8 @@ const pointer = t.type({

const addressing = t.type({
change: t.number,
index: t.number
index: t.number,
accountIndex: t.number
})

const value = t.type({
Expand Down
4 changes: 2 additions & 2 deletions src/Transaction/index.ts
@@ -1,6 +1,6 @@
import { Transaction, buildTransaction } from './Transaction'
import { Transaction } from './Transaction'
import { TransactionInput } from './TransactionInput'
import { TransactionOutput } from './TransactionOutput'

export default Transaction
export { TransactionInput, TransactionOutput, buildTransaction }
export { TransactionInput, TransactionOutput }
34 changes: 20 additions & 14 deletions src/Utils/address_discovery.spec.ts
@@ -1,53 +1,59 @@
import { expect } from 'chai'
import { InMemoryKeyManager } from '../KeyManager'
import { generateMnemonic } from './mnemonic'
import { AddressType } from '../Wallet'
import { addressDiscoveryWithinBounds } from './address_discovery'
import { InMemoryKeyManager, RustCardano } from '../lib'
import { ChainSettings } from '../Cardano'

describe('addressDiscovery', async () => {
const mnemonic = generateMnemonic()
const account = await InMemoryKeyManager({ mnemonic, password: 'foobar' }).publicAccount()
describe('addressDiscovery', () => {
let mnemonic: string
let account: string

beforeEach(async () => {
mnemonic = generateMnemonic()
account = await InMemoryKeyManager(RustCardano, { mnemonic, password: 'foobar' }).publicParentKey()
})

it('correctly returns address indexes and address type', () => {
const internalAddresses = addressDiscoveryWithinBounds({ account, type: AddressType.internal, lowerBound: 0, upperBound: 19 })
const internalAddresses = addressDiscoveryWithinBounds(RustCardano, { account, type: AddressType.internal, lowerBound: 0, upperBound: 19 }, ChainSettings.mainnet)
expect(internalAddresses[0].index).to.eql(0)
expect(internalAddresses[0].type).to.eql(AddressType.internal)

const externalAddresses = addressDiscoveryWithinBounds({ account, type: AddressType.external, lowerBound: 0, upperBound: 19 })
const externalAddresses = addressDiscoveryWithinBounds(RustCardano, { account, type: AddressType.external, lowerBound: 0, upperBound: 19 }, ChainSettings.mainnet)
expect(externalAddresses[0].index).to.eql(0)
expect(externalAddresses[0].type).to.eql(AddressType.external)
})

describe('internal', () => {
it('discovers addresses between bounds', () => {
const internalAddresses = addressDiscoveryWithinBounds({ account, type: AddressType.internal, lowerBound: 0, upperBound: 19 })
const internalAddresses = addressDiscoveryWithinBounds(RustCardano, { account, type: AddressType.internal, lowerBound: 0, upperBound: 19 }, ChainSettings.mainnet)
expect(internalAddresses.length).to.eql(20)
})

it('does not collide between different bounds', () => {
const first20Addresses = addressDiscoveryWithinBounds({ account, type: AddressType.internal, lowerBound: 0, upperBound: 19 })
const next20Addresses = addressDiscoveryWithinBounds({ account, type: AddressType.internal, lowerBound: 20, upperBound: 39 })
const first20Addresses = addressDiscoveryWithinBounds(RustCardano, { account, type: AddressType.internal, lowerBound: 0, upperBound: 19 }, ChainSettings.mainnet)
const next20Addresses = addressDiscoveryWithinBounds(RustCardano, { account, type: AddressType.internal, lowerBound: 20, upperBound: 39 }, ChainSettings.mainnet)
const addressSet = new Set(first20Addresses.concat(next20Addresses))
expect([...addressSet].length).to.eql(40)
})

it('does not collide with external addresses', () => {
const internalAddresses = addressDiscoveryWithinBounds({ account, type: AddressType.internal, lowerBound: 0, upperBound: 19 })
const externalAddresses = addressDiscoveryWithinBounds({ account, type: AddressType.external, lowerBound: 0, upperBound: 19 })
const internalAddresses = addressDiscoveryWithinBounds(RustCardano, { account, type: AddressType.internal, lowerBound: 0, upperBound: 19 }, ChainSettings.mainnet)
const externalAddresses = addressDiscoveryWithinBounds(RustCardano, { account, type: AddressType.external, lowerBound: 0, upperBound: 19 }, ChainSettings.mainnet)
const addressSet = new Set(internalAddresses.concat(externalAddresses))
expect([...addressSet].length).to.eql(40)
})
})

describe('external', () => {
it('discovers addresses between bounds', () => {
const externalAddresses = addressDiscoveryWithinBounds({ account, type: AddressType.external, lowerBound: 0, upperBound: 19 })
const externalAddresses = addressDiscoveryWithinBounds(RustCardano, { account, type: AddressType.external, lowerBound: 0, upperBound: 19 }, ChainSettings.mainnet)
expect(externalAddresses.length).to.eql(20)
})

it('does not collide between different bounds', () => {
const first20Addresses = addressDiscoveryWithinBounds({ account, type: AddressType.external, lowerBound: 0, upperBound: 19 })
const next20Addresses = addressDiscoveryWithinBounds({ account, type: AddressType.external, lowerBound: 20, upperBound: 39 })
const first20Addresses = addressDiscoveryWithinBounds(RustCardano, { account, type: AddressType.external, lowerBound: 0, upperBound: 19 }, ChainSettings.mainnet)
const next20Addresses = addressDiscoveryWithinBounds(RustCardano, { account, type: AddressType.external, lowerBound: 20, upperBound: 39 }, ChainSettings.mainnet)
const addressSet = new Set(first20Addresses.concat(next20Addresses))
expect([...addressSet].length).to.eql(40)
})
Expand Down

0 comments on commit f744f78

Please sign in to comment.