gitiumiota
Version:
IOTA Client Reference Implementation
315 lines (280 loc) • 10.6 kB
text/typescript
import { addEntry, addSignatureOrMessage, finalizeBundle } from '@iota/bundle'
import { removeChecksum } from '@iota/checksum'
import { tritsToTrytes, trytesToTrits, valueToTrits } from '@iota/converter'
import { Balances, createGetBalances } from '@iota/core'
import Kerl from '@iota/kerl'
import {
digests,
FRAGMENT_LENGTH,
key,
NORMALIZED_FRAGMENT_LENGTH,
normalizedBundle,
signatureFragment,
subseed,
} from '@iota/signing'
import {
address,
bundle as bundleHash,
SIGNATURE_OR_MESSAGE_LENGTH,
SIGNATURE_OR_MESSAGE_OFFSET,
signatureOrMessage,
TRANSACTION_LENGTH,
} from '@iota/transaction'
import * as Promise from 'bluebird'
import * as errors from '../../errors'
import {
arrayValidator,
isHash,
isNinesTrytes,
isSecurityLevel,
remainderAddressValidator,
transferValidator,
validate,
Validator,
} from '../../guards'
import { Bundle, Callback, Provider, Transaction, Transfer } from '../../types'
import Address from './address'
export { Bundle, Callback, Provider, Transaction, Transfer }
export interface MultisigInput {
readonly address: string
readonly balance: number
readonly securitySum: number
}
export const multisigInputValidator: Validator<MultisigInput> = (multisigInput: any) => [
multisigInput,
(input: MultisigInput) =>
isSecurityLevel(input.securitySum) &&
isHash(input.address) &&
Number.isInteger(input.balance) &&
input.balance > 0,
errors.INVALID_INPUT,
]
export const sanitizeTransfers = (transfers: ReadonlyArray<Transfer>): ReadonlyArray<Transfer> =>
transfers.map(transfer => ({
...transfer,
message: transfer.message || '',
tag: transfer.tag || '',
address: removeChecksum(transfer.address),
}))
/* tslint:disable:variable-name */
export const createBundle = (
input: MultisigInput,
transfers: ReadonlyArray<Transfer>,
remainderAddress?: string
): Int8Array => {
// Create a new bundle
let bundle: Int8Array = transfers.reduce((acc, transfer) => {
const message = trytesToTrits(transfer.message || '')
const signatureOrMessageTrits = new Int8Array(
(1 + Math.floor(message.length / SIGNATURE_OR_MESSAGE_LENGTH)) * SIGNATURE_OR_MESSAGE_LENGTH
)
signatureOrMessageTrits.set(message, SIGNATURE_OR_MESSAGE_OFFSET)
return addEntry(acc, {
signatureOrMessage: signatureOrMessageTrits,
address: trytesToTrits(removeChecksum(transfer.address)),
value: valueToTrits(transfer.value),
obsoleteTag: trytesToTrits(transfer.tag || ''),
issuanceTimestamp: valueToTrits(Math.floor(Date.now() / 1000)),
})
}, new Int8Array(0))
const totalBalance = input.balance
const totalValue = transfers.reduce((acc, transfer) => (acc += transfer.value), 0)
const remainder = totalBalance - totalValue
if (remainder < 0) {
throw new Error('Not enough balance.')
}
if (totalBalance > 0) {
// Add input as bundle entry
// Only a single entry, signatures will be added later
bundle = addEntry(bundle, {
signatureOrMessage: new Int8Array(input.securitySum * SIGNATURE_OR_MESSAGE_LENGTH),
address: trytesToTrits(input.address),
value: valueToTrits(0 - totalBalance),
issuanceTimestamp: valueToTrits(Math.floor(Date.now() / 1000)),
})
}
// If there is a remainder value
// Add extra output to send remaining funds to
if (remainder > 0) {
// Remainder bundle entry if necessary
if (!remainderAddress) {
throw new Error('No remainder address defined')
}
bundle = addEntry(bundle, {
signatureOrMessage: new Int8Array(SIGNATURE_OR_MESSAGE_LENGTH),
address: trytesToTrits(remainderAddress),
value: valueToTrits(remainder),
issuanceTimestamp: valueToTrits(Math.floor(Date.now() / 1000)),
})
}
return finalizeBundle(bundle)
}
/**
* @class Multisig
*
* @memberof module:multisig
*/
export default class Multisig {
public address = Address
private provider: Provider // tslint:disable-line variable-name
constructor(provider: Provider) {
this.provider = provider
}
/**
* Gets the key value of a seed
*
* @member getKey
*
* @memberof Multisig
*
* @param {string} seed
* @param {number} index
* @param {number} security Security level to be used for the private key / address. Can be 1, 2 or 3
*
* @return {Int8Array} digest trytes
*/
public getKey(seed: string, index: number, security: number) {
return key(subseed(trytesToTrits(seed), index), security)
}
/**
* Gets the digest value of a seed
*
* @member getDigest
*
* @memberof Multisig
*
* @param {string} seed
* @param {number} index
* @param {number} security Security level to be used for the private key / address. Can be 1, 2 or 3
*
* @return {string} digest trytes
**/
public getDigest(seed: string, index: number, security: number) {
const keyTrits = key(subseed(trytesToTrits(seed), index), security)
return tritsToTrytes(digests(keyTrits))
}
/**
* Validates a generated multisig address
*
* @member validateAddress
*
* @memberof Multisig
*
* @param {string} multisigAddress
* @param {array} digests
*
* @return {boolean}
*/
public validateAddress(multisigAddress: string, digestsArr: ReadonlyArray<string>) {
const kerl = new Kerl()
// initialize Kerl with the provided state
kerl.initialize()
// Absorb all key digests
digestsArr.forEach(keyDigest => {
const digestTrits = trytesToTrits(keyDigest)
kerl.absorb(trytesToTrits(keyDigest), 0, digestTrits.length)
})
// Squeeze address trits
const addressTrits: Int8Array = new Int8Array(Kerl.HASH_LENGTH)
kerl.squeeze(addressTrits, 0, Kerl.HASH_LENGTH)
// Convert trits into trytes and return the address
return tritsToTrytes(addressTrits) === multisigAddress
}
/**
* Prepares transfer by generating the bundle with the corresponding cosigner transactions
* Does not contain signatures
*
* @member initiateTransfer
*
* @memberof Multisig
*
* @param {object} input the input addresses as well as the securitySum, and balance where:
* - `address` is the input multisig address
* - `securitySum` is the sum of security levels used by all co-signers
* - `balance` is the expected balance, if you wish to override getBalances
* @param {string} remainderAddress Has to be generated by the cosigners before initiating the transfer, can be null if fully spent
* @param {object} transfers
* @param {function} callback
*
* @return {Int8Array} Bundle trits
*/
public initiateTransfer(
input: MultisigInput,
transfers: ReadonlyArray<Transfer>,
remainderAddress?: string,
callback?: Callback<Bundle>
): Promise<Bundle> {
return Promise.resolve(
validate(
multisigInputValidator(input),
arrayValidator<Transfer>(transferValidator)(transfers),
!!remainderAddress && remainderAddressValidator(remainderAddress)
)
)
.then(() => sanitizeTransfers(transfers))
.then((sanitizedTransfers: ReadonlyArray<Transfer>) =>
input.balance
? createBundle(input, sanitizedTransfers, remainderAddress)
: (createGetBalances(this.provider) as any)([input.address], 100)
.then(
(res: Balances): MultisigInput => ({
...input,
balance: res.balances[0],
})
)
.then((inputWithBalance: MultisigInput) =>
createBundle(inputWithBalance, sanitizedTransfers, remainderAddress)
)
)
.asCallback(callback)
}
/**
* Adds the cosigner signatures to the corresponding bundle transaction
*
* @member addSignature
*
* @memberof Multisig
*
* @param {Int8Array} bundle
* @param {number} cosignerIndex
* @param {string} inputAddress
* @param {string} keyTrits
* @param {function} callback
*
* @return {Int8Array} bundle with signature trits
*/
public addSignature(bundle: Int8Array, inputAddress: string, keyTrits: Int8Array, callback: Callback) {
const bundleHashTrits = bundleHash(bundle)
const normalizedBundleHash = normalizedBundle(bundleHashTrits)
let signatureIndex = 0
for (const offset = 0; offset < bundle.length * TRANSACTION_LENGTH; offset + TRANSACTION_LENGTH) {
if (tritsToTrytes(address(bundle)) === inputAddress && isNinesTrytes(signatureOrMessage(bundle))) {
const signature = new Int8Array(keyTrits.length)
for (let i = 0; i < keyTrits.length / FRAGMENT_LENGTH; i++) {
signature.set(
signatureFragment(
normalizedBundleHash.slice(
i * NORMALIZED_FRAGMENT_LENGTH,
(i + 1) * NORMALIZED_FRAGMENT_LENGTH
),
keyTrits.slice(i * FRAGMENT_LENGTH, (i + 1) * FRAGMENT_LENGTH)
),
i * FRAGMENT_LENGTH
)
}
const bundleTrits = addSignatureOrMessage(bundle, signature, signatureIndex)
const bundleTrytes = []
for (let jOffset = 0; jOffset < bundleTrits.length; jOffset += TRANSACTION_LENGTH) {
bundleTrytes.push(tritsToTrytes(bundleTrits.slice(jOffset, jOffset + TRANSACTION_LENGTH)))
}
return callback(null, bundleTrytes.slice())
}
signatureIndex += 1
}
return callback(new Error('Could not find signature index for address: ' + inputAddress))
}
}
/**
* Multisig address constructor
*/
Multisig.prototype.address = Address