gitiumiota
Version:
IOTA Client Reference Implementation
446 lines (406 loc) • 18.3 kB
text/typescript
import {
CDA,
CDA_CHECKSUM_LENGTH,
CDAInput,
CDAParams,
CDATransfer,
deserializeCDA,
deserializeCDAInput,
isExpired,
serializeCDAInput,
verifyCDAParams,
verifyCDATransfer,
} from '@iota/cda'
import { tritsToTrytes, tritsToValue, TRYTE_WIDTH, trytesToTrits } from '@iota/converter'
import {
createAttachToTangle,
createCheckConsistency,
createFindTransactions,
createGetBalances,
createGetBundlesFromAddresses,
createGetLatestInclusion,
createGetTransactionsToApprove,
createGetTrytes,
createPrepareTransfers,
createSendTrytes,
createStoreAndBroadcast,
isAboveMaxDepth,
} from '@iota/core'
import { createHttpClient } from '@iota/http-client'
import { PersistenceBatchTypes } from '@iota/persistence'
import { createPersistenceAdapter } from '@iota/persistence-adapter-level'
import { address as signingAddress, digests, key, subseed } from '@iota/signing'
import {
address as transactionAddress,
bundle as bundleHash,
BUNDLE_LENGTH,
BUNDLE_OFFSET,
TRANSACTION_LENGTH,
transactionHash,
} from '@iota/transaction'
import { asTransactionObject } from '@iota/transaction-converter'
import * as Promise from 'bluebird'
import '../../typed-array'
import { Bundle, Hash, PersistenceDelCommand, Transaction, Trytes } from '../../types'
import {
AccountPreset,
AddressGeneration,
AddressGenerationParams,
Network,
NetworkParams,
TransactionAttachment,
TransactionAttachmentParams,
TransactionAttachmentStartParams,
TransactionIssuance,
TransactionIssuanceParams,
} from './account'
export enum Events {
selectInput = 'selectedInput',
prepareTransfer = 'preparedTransfer',
getTransactionsToApprove = 'getTransactionsToApprove',
attachToTangle = 'attachToTangle',
broadcast = 'broadcast',
error = 'error',
}
export interface CDAAccount
extends AddressGeneration<CDAParams, CDA>,
TransactionIssuance<CDA, Bundle>,
TransactionAttachment {}
export function networkAdapter({ provider }: NetworkParams): Network {
const httpClient = createHttpClient({ provider })
const getBalances = createGetBalances(httpClient)
const getTrytes = createGetTrytes(httpClient)
const getLatestInclusion = createGetLatestInclusion(httpClient)
return {
getTrytes: hashes => (hashes.length > 0 ? getTrytes(hashes) : Promise.resolve([])),
getBalance: (address): Promise<number> => getBalances([address], 100).then(({ balances }) => balances[0]),
getBalances,
getConsistency: createCheckConsistency(httpClient),
getLatestInclusion: hashes => (hashes.length > 0 ? getLatestInclusion(hashes) : Promise.resolve([])),
getBundlesFromAddresses: createGetBundlesFromAddresses(httpClient),
findTransactions: createFindTransactions(httpClient),
sendTrytes: createSendTrytes(httpClient),
setSettings: httpClient.setSettings,
getTransactionsToApprove: createGetTransactionsToApprove(httpClient),
attachToTangle: createAttachToTangle(httpClient),
storeAndBroadcast: createStoreAndBroadcast(httpClient),
}
}
export function addressGeneration(addressGenerationParams: AddressGenerationParams) {
const { seed, persistence, timeSource } = addressGenerationParams
return {
generateCDA(cdaParams: CDAParams) {
if (!cdaParams) {
throw new Error(
'Provide an object with conditions for the CDA: { timeoutAt, [multiUse], [exeptectedAmount], [security=2] }'
)
}
const { timeoutAt, expectedAmount, multiUse } = cdaParams
return Promise.try(() => timeSource().then(currentTime => verifyCDAParams(currentTime, cdaParams)))
.then(persistence.increment)
.then(index => {
const security = cdaParams.security || addressGenerationParams.security
const address = signingAddress(digests(key(subseed(seed, tritsToValue(index)), security)))
const serializedCDA = serializeCDAInput({
address,
index,
security,
timeoutAt,
multiUse,
expectedAmount,
})
return persistence
.put(['0', ':', tritsToTrytes(address)].join(), serializedCDA)
.then(() => deserializeCDA(serializedCDA))
})
},
}
}
interface CDAInputs {
inputs: CDAInput[]
totalBalance: number
}
export function transactionIssuance(
this: any,
{ seed, deposits, persistence, network, timeSource, now }: TransactionIssuanceParams
) {
const { getBalance } = network
const prepareTransfers = createPrepareTransfers(undefined, now)
const transactionIssuer = {
sendToCDA: (cdaTransfer: CDATransfer): Promise<ReadonlyArray<Trytes>> => {
if (!cdaTransfer) {
throw new Error(
'Provide an object with conditions and value for the CDA transfer: { timeoutAt, [multiUse], [exeptectedAmount], [security=2], value }'
)
}
return Promise.try(() =>
persistence
.ready()
.then(timeSource)
.then(currentTime => verifyCDATransfer(currentTime, cdaTransfer))
)
.then(() => accumulateInputs(cdaTransfer.value))
.then(({ inputs, totalBalance }) => {
inputs.forEach(input => this.emit(Events.selectInput, { cdaTransfer, input }))
const remainder = totalBalance - cdaTransfer.value
return generateRemainderAddress(remainder).then(remainderAddress =>
prepareTransfers(
seed,
[
{
address: cdaTransfer.address.slice(0, -(CDA_CHECKSUM_LENGTH / TRYTE_WIDTH)),
value: cdaTransfer.value,
},
],
{
inputs: inputs.map(input => ({
address: tritsToTrytes(input.address),
keyIndex: tritsToValue(input.index),
security: input.security,
balance: input.balance as number,
})),
remainderAddress,
}
)
.tap(trytes => this.emit(Events.prepareTransfer, { cdaTransfer, trytes }))
.tap(trytes =>
persistence.batch([
...inputs.map(
(input): PersistenceDelCommand<string> => ({
type: PersistenceBatchTypes.del,
key: ['0', ':', tritsToTrytes(input.address)].join(''),
})
),
{
type: PersistenceBatchTypes.put,
key: [
'0',
':',
tritsToTrytes(bundleHash(bundleTrytesToBundleTrits(trytes))),
].join(''),
value: bundleTrytesToBundleTrits(trytes),
},
])
)
)
})
},
}
function accumulateInputs(
threshold: number,
acc: CDAInputs = { inputs: [], totalBalance: 0 },
buffer: Int8Array[] = []
): Promise<CDAInputs> {
if (deposits.inboundLength() === 0) {
buffer.forEach(deposits.write)
throw new Error('Insufficient balance')
}
return deposits.read().then(cda =>
timeSource().then(currentTime => {
const input = deserializeCDAInput(cda)
return getBalance(tritsToTrytes(input.address)).then(balance => {
//
// Input selection Conditions
//
// The following strategy is blocking execution because it awaits arrival of balance on inputs.
// A strategy leading to eventual input selection should be discussed.
// Such us inputs are selected prior to inclusion of funding transactions,
// and order of funding and withdrawing is not important.
// This would allow for _transduction_ of transfers instead of reduction of inputs.
//
if (balance > 0) {
if (input.expectedAmount && balance >= input.expectedAmount) {
acc.totalBalance += balance
acc.inputs.push({ ...input, balance })
} else if (input.multiUse && isExpired(currentTime, input)) {
acc.totalBalance += balance
acc.inputs.push({ ...input, balance })
} else if (!input.multiUse) {
acc.totalBalance += balance
acc.inputs.push({ ...input, balance })
}
} else if (input.timeoutAt !== 0 && isExpired(currentTime, input)) {
persistence.del(['0', ':', tritsToTrytes(input.address)].join(''))
} else {
buffer.push(cda)
}
return acc.totalBalance >= threshold ? acc : accumulateInputs(threshold, acc, buffer)
})
})
)
}
function generateRemainderAddress(remainder: number): Promise<Trytes | undefined> {
if (remainder === 0) {
return Promise.resolve(undefined)
}
return persistence.increment().then(index => {
const security = 2
const remainderAddress = signingAddress(digests(key(subseed(seed, tritsToValue(index)), security)))
return persistence
.put(
['0', ':', tritsToTrytes(remainderAddress)].join(''),
serializeCDAInput({
address: remainderAddress,
index,
security,
timeoutAt: 0,
multiUse: false,
expectedAmount: remainder,
})
)
.then(() => tritsToTrytes(remainderAddress))
})
}
return transactionIssuer
}
export function transactionAttachment(this: any, params: TransactionAttachmentParams): TransactionAttachment {
const { bundles, persistence, network } = params
const {
findTransactions,
storeAndBroadcast,
getTransactionsToApprove,
attachToTangle,
getTrytes,
getLatestInclusion,
getConsistency,
} = network
let reference: Transaction
let running = false
const attachToTangleRoutine = (attachParams: TransactionAttachmentStartParams) => {
if (!running) {
return false
}
const { depth, minWeightMagnitude, maxDepth, delay } = attachParams
bundles.read().then(bundle =>
Promise.resolve({ addresses: [tritsToTrytes(transactionAddress(bundle.slice(-TRANSACTION_LENGTH)))] })
.then(findTransactions)
.then(getTrytes)
.then(pastAttachments =>
pastAttachments.filter(
trytes =>
tritsToTrytes(bundleHash(bundle)) ===
trytes.slice(
BUNDLE_OFFSET / TRYTE_WIDTH,
BUNDLE_OFFSET / TRYTE_WIDTH + BUNDLE_LENGTH / TRYTE_WIDTH
)
)
)
.then(pastAttachments =>
pastAttachments.map(trytes => tritsToTrytes(transactionHash(trytesToTrits(trytes))))
)
.then(pastAttachmentHashes =>
getLatestInclusion(pastAttachmentHashes).tap(inclusionStates => {
if (inclusionStates.indexOf(true) > -1) {
return persistence.del(['0', ':', tritsToTrytes(bundleHash(bundle))].join(''))
}
return Promise.all(pastAttachmentHashes.map(h => getConsistency([h]))).tap(consistencyStates =>
consistencyStates.indexOf(true) > -1
? setTimeout(() => bundles.write(bundle), delay)
: getTransactionsToApprove(depth, reference ? reference.hash : undefined)
.tap(transactionsToApprove =>
this.emit(Events.getTransactionsToApprove, {
trytes: bundleTritsToBundleTrytes(bundle),
transactionsToApprove,
})
)
.then(
({
trunkTransaction,
branchTransaction,
}: {
trunkTransaction: Hash
branchTransaction: Hash
}) =>
attachToTangle(
trunkTransaction,
branchTransaction,
minWeightMagnitude,
bundleTritsToBundleTrytes(bundle)
)
)
.tap(transactions =>
this.emit(
Events.attachToTangle,
transactions.map(t => asTransactionObject(t))
)
)
.then(attachedTrytes => storeAndBroadcast(attachedTrytes))
.tap(attachedTrytes =>
this.emit(Events.broadcast, attachedTrytes.map(t => asTransactionObject(t)))
)
.then(attachedTrytes => attachedTrytes.map(t => asTransactionObject(t)))
.tap(([tail]) => {
if (!reference || !isAboveMaxDepth(reference.attachmentTimestamp, maxDepth)) {
reference = tail
} else {
return getConsistency([tail.hash]).then(consistent => {
if (!consistent) {
reference = tail
}
})
}
setTimeout(() => bundles.write(bundle), delay)
})
)
})
)
.tap(() => setTimeout(() => attachToTangleRoutine(attachParams), 1000))
.catch(error => {
bundles.write(bundle)
this.emit(Events.error, error)
})
)
}
return {
startAttaching: (startParams: TransactionAttachmentStartParams) => {
if (running) {
return
}
running = true
attachToTangleRoutine(startParams)
},
stopAttaching: () => {
if (!running) {
return
}
running = false
},
}
}
export function createAccountPreset(test = {}): AccountPreset<CDAParams, CDA, ReadonlyArray<Trytes>> {
return {
persistencePath: './',
persistenceAdapter: createPersistenceAdapter,
provider: 'http://localhost:14265',
network: networkAdapter,
security: 2,
addressGeneration,
transactionIssuance,
transactionAttachment,
timeSource: () => Promise.resolve(Math.floor(Date.now() / 1000)),
depth: 3,
minWeightMagnitude: 9,
delay: 1000 * 30,
pollingDelay: 1000 * 30,
maxDepth: 6,
test,
}
}
export const preset = createAccountPreset()
export const testPreset = createAccountPreset({
now: () => 1,
})
function bundleTritsToBundleTrytes(trits: Int8Array): ReadonlyArray<Trytes> {
const out = []
for (let offset = 0; offset < trits.length; offset += TRANSACTION_LENGTH) {
out.push(tritsToTrytes(trits.slice(offset, offset + TRANSACTION_LENGTH)))
}
return out
}
function bundleTrytesToBundleTrits(trytes: ReadonlyArray<Trytes>): Int8Array {
const out = new Int8Array(trytes.length * TRANSACTION_LENGTH)
for (let i = 0; i < trytes.length; i++) {
out.set(trytesToTrits(trytes[i]), i * TRANSACTION_LENGTH)
}
return out
}