UNPKG

rainbow-bridge-lib

Version:
475 lines (437 loc) 13.4 kB
const Web3 = require('web3') const nearlib = require('near-api-js') const bs58 = require('bs58') const BN = require('bn.js') const { TextDecoder } = require('util') const { borshifyOutcomeProof } = require('./borshify-proof') const { sleep, backoff, signAndSendTransaction } = require('./robust') class BorshError extends Error { constructor(message) { super(message) this.name = this.constructor.name } } function serializeField(schema, value, fieldType, writer) { if (fieldType === 'u8') { writer.write_u8(value) } else if (fieldType === 'u64') { writer.write_u64(value) } else if (fieldType === 'u128') { writer.write_u128(value) } else if (fieldType === 'bool') { return writer.write_u8(value ? 1 : 0) } else if (fieldType === 'string') { return writer.write_string(value) } else if (fieldType instanceof Array) { if (typeof fieldType[0] === 'number') { if (value.length !== fieldType[0]) { throw new Error( `Expecting byte array of length ${fieldType[0]}, but got ${value.length} bytes` ) } writer.write_fixed_array(value) } else { writer.write_array(value, (item) => { serializeField(schema, item, fieldType[0], writer) }) } } else { const structSchema = schema[fieldType] if (!structSchema) { throw new Error(`Schema type ${fieldType} is missing in schema`) } if (structSchema.kind === 'option') { if (value === null) { writer.write_u8(0) } else { writer.write_u8(1) serializeField(schema, value, structSchema.type, writer) } } else if (structSchema.kind === 'struct') { structSchema.fields.map(([fieldName, fieldType]) => { serializeField(schema, value[fieldName], fieldType, writer) }) } else if (structSchema.kind === 'function') { writer.write_buffer(structSchema.ser(value)) } else { throw new Error( `Unexpected schema kind: ${structSchema.kind} for ${fieldType}` ) } } } function deserializeField(schema, fieldType, reader) { if (fieldType === 'u8') { return reader.read_u8() } else if (fieldType === 'u64') { return reader.read_u64() } else if (fieldType === 'u128') { return reader.read_u128() } else if (fieldType === 'bool') { return !!reader.read_u8() } else if (fieldType === 'string') { return reader.read_string() } else if (fieldType instanceof Array) { if (typeof fieldType[0] === 'number') { return reader.read_fixed_array(fieldType[0]) } else { return reader.read_array(() => deserializeField(schema, fieldType[0], reader) ) } } else { const structSchema = schema[fieldType] if (!structSchema) { throw new Error(`Schema type ${fieldType} is missing in schema`) } if (structSchema.kind === 'option') { const optionRes = reader.read_u8() if (optionRes === 0) { return null } else if (optionRes === 1) { return deserializeField(schema, structSchema.type, reader) } else { throw new Error(`Unexpected option flag: ${optionRes}`) } } else if (structSchema.kind === 'struct') { const result = {} for (const [fieldName, fieldType] of structSchema.fields) { result[fieldName] = deserializeField(schema, fieldType, reader) } return result } else if (structSchema.kind === 'function') { return structSchema.deser(reader, schema) } else { throw new Error( `Unexpected schema kind: ${structSchema.kind} for ${fieldType}` ) } } } /// Serialize given object using schema of the form: /// { class_name -> [ [field_name, field_type], .. ], .. } function serialize(schema, fieldType, obj) { if (fieldType === null) { return new Uint8Array() } const writer = new nearlib.utils.serialize.BinaryWriter() serializeField(schema, obj, fieldType, writer) return writer.toArray() } class BinaryReader { constructor(buf) { this.buf = buf this.offset = 0 } read_u8() { const value = this.buf.readUInt8(this.offset) this.offset += 1 return value } read_u32() { const value = this.buf.readUInt32LE(this.offset) this.offset += 4 return value } read_u64() { const buf = this.read_buffer(8) return new BN(buf, 'le') } read_u128() { const buf = this.read_buffer(16) return new BN(buf, 'le') } read_buffer(len) { if (this.offset + len > this.buf.length) { throw new BorshError(`Expected buffer length ${len} isn't within bounds`) } const result = this.buf.slice(this.offset, this.offset + len) this.offset += len return result } read_string() { const len = this.read_u32() const buf = this.read_buffer(len) // @ts-ignore const textDecoder = TextDecoder() try { // NOTE: Using TextDecoder to fail on invalid UTF-8 return textDecoder.decode(buf) } catch (e) { throw new BorshError(`Error decoding UTF-8 string: ${e}`) } } read_fixed_array(len) { return new Uint8Array(this.read_buffer(len)) } read_array(fn) { const len = this.read_u32() const result = [] for (let i = 0; i < len; ++i) { result.push(fn()) } return result } } function deserialize(schema, fieldType, buffer) { if (fieldType === null) { return null } const reader = new BinaryReader(buffer) const result = deserializeField(schema, fieldType, reader) if (reader.offset < buffer.length) { throw new Error( `Unexpected ${ buffer.length - reader.offset } bytes after deserialized data` ) } return result } const DEFAULT_FUNC_CALL_AMOUNT = new BN('300000000000000') const RETRY_TX_STATUS = 10 const signAndSendTransactionAsync = async ( accessKey, account, receiverId, actions ) => { const status = await account.connection.provider.status() let [txHash, signedTx] = await nearlib.transactions.signTransaction( receiverId, ++accessKey.nonce, actions, nearlib.utils.serialize.base_decode(status.sync_info.latest_block_hash), account.connection.signer, account.accountId, account.connection.networkId ) const bytes = signedTx.encode() await account.connection.provider.sendJsonRpc('broadcast_tx_async', [ Buffer.from(bytes).toString('base64'), ]) console.log('TxHash', nearlib.utils.serialize.base_encode(txHash)) return txHash } const txnStatus = async ( account, txHash, retries = RETRY_TX_STATUS, wait = 1000 ) => { let result for (let i = 0; i < retries; i++) { try { result = await account.connection.provider.txStatus( txHash, account.accountId ) if ( result.status.SuccessValue !== undefined || result.status.Failure !== undefined ) { break } } catch (e) { await sleep(wait) } } if (!result) { throw new Error( `Transaction ${txHash} didn't finish after ${ (retries * wait) / 1000 } seconds` ) } const flatLogs = [ result.transaction_outcome, ...result.receipts_outcome, ].reduce((acc, it) => acc.concat(it.outcome.logs), []) if (flatLogs && flatLogs != []) { console.log(flatLogs) } if (result.status.SuccessValue !== undefined) { return result } throw new Error(JSON.stringify(result.status.Failure)) } function getBorshTransactionLastResult(txResult) { return txResult && Buffer.from(txResult.status.SuccessValue, 'base64') } class BorshContract { constructor(borshSchema, account, contractId, options) { this.account = account this.contractId = contractId options.viewMethods.forEach((d) => { Object.defineProperty(this, d.methodName, { writable: false, enumerable: true, value: async (args) => { args = serialize(borshSchema, d.inputFieldType, args) const result = await backoff(10, () => this.account.connection.provider.query( `call/${this.contractId}/${d.methodName}`, nearlib.utils.serialize.base_encode(args) ) ) if (result.logs) { this.account.printLogs(this.contractId, result.logs) } return ( result.result && result.result.length > 0 && deserialize( borshSchema, d.outputFieldType, Buffer.from(result.result) ) ) }, }) }) options.changeMethods.forEach((d) => { Object.defineProperty(this, d.methodName, { writable: false, enumerable: true, value: async (args, gas, amount) => { args = serialize(borshSchema, d.inputFieldType, args) const rawResult = await signAndSendTransaction( this.accessKey, this.account, this.contractId, [ nearlib.transactions.functionCall( d.methodName, Buffer.from(args), gas || DEFAULT_FUNC_CALL_AMOUNT, amount ), ] ) const result = getBorshTransactionLastResult(rawResult) return result && deserialize(borshSchema, d.outputFieldType, result) }, }) }) options.changeMethods.forEach((d) => { Object.defineProperty(this, d.methodName + '_async', { writable: false, enumerable: true, value: async (args, gas, amount) => { args = serialize(borshSchema, d.inputFieldType, args) return await signAndSendTransactionAsync( this.accessKey, this.account, this.contractId, [ nearlib.transactions.functionCall( d.methodName, Buffer.from(args), gas || DEFAULT_FUNC_CALL_AMOUNT, amount ), ] ) }, }) }) } async accessKeyInit() { await this.account.ready this.accessKey = await this.account.findAccessKey() if (!this.accessKey) { // @ts-ignore throw new Error( `Can not sign transactions for account ${this.account.accountId}, no matching key pair found in Signer.`, 'KeyNotFound' ) } } } function borshify(block) { return Buffer.concat([ bs58.decode(block.prev_block_hash), bs58.decode(block.next_block_inner_hash), Buffer.concat([ // @ts-ignore Web3.utils.toBN(block.inner_lite.height).toBuffer('le', 8), bs58.decode(block.inner_lite.epoch_id), bs58.decode(block.inner_lite.next_epoch_id), bs58.decode(block.inner_lite.prev_state_root), bs58.decode(block.inner_lite.outcome_root), // @ts-ignore Web3.utils .toBN(block.inner_lite.timestamp_nanosec || block.inner_lite.timestamp) .toBuffer('le', 8), bs58.decode(block.inner_lite.next_bp_hash), bs58.decode(block.inner_lite.block_merkle_root), ]), bs58.decode(block.inner_rest_hash), Buffer.from([1]), // @ts-ignore Web3.utils.toBN(block.next_bps.length).toBuffer('le', 4), Buffer.concat( block.next_bps.map((nextBp) => Buffer.concat([ // @ts-ignore Web3.utils.toBN(nextBp.account_id.length).toBuffer('le', 4), Buffer.from(nextBp.account_id), nextBp.public_key.substr(0, 8) === 'ed25519:' ? Buffer.from([0]) : Buffer.from([1]), bs58.decode(nextBp.public_key.substr(8)), // @ts-ignore Web3.utils.toBN(nextBp.stake).toBuffer('le', 16), ]) ) ), // @ts-ignore Web3.utils.toBN(block.approvals_after_next.length).toBuffer('le', 4), Buffer.concat( block.approvals_after_next.map((signature) => signature === null ? Buffer.from([0]) : Buffer.concat([ Buffer.from([1]), signature.substr(0, 8) === 'ed25519:' ? Buffer.from([0]) : Buffer.from([1]), bs58.decode(signature.substr(8)), ]) ) ), ]) } function borshifyInitialValidators(initialValidators) { return Buffer.concat([ Web3.utils.toBN(initialValidators.length).toBuffer('le', 4), Buffer.concat( initialValidators.map((nextBp) => Buffer.concat([ Web3.utils.toBN(nextBp.account_id.length).toBuffer('le', 4), Buffer.from(nextBp.account_id), nextBp.public_key.substr(0, 8) === 'ed25519:' ? Buffer.from([0]) : Buffer.from([1]), bs58.decode(nextBp.public_key.substr(8)), Web3.utils.toBN(nextBp.stake).toBuffer('le', 16), ]) ) ), ]) } // @ts-ignore const hexToBuffer = (hex) => Buffer.from(Web3.utils.hexToBytes(hex)) // @ts-ignore const readerToHex = (len) => (reader) => Web3.utils.bytesToHex(reader.read_fixed_array(len)) exports.BorshContract = BorshContract exports.hexToBuffer = hexToBuffer exports.readerToHex = readerToHex exports.borshifyOutcomeProof = borshifyOutcomeProof exports.txnStatus = txnStatus exports.borshify = borshify exports.borshifyInitialValidators = borshifyInitialValidators exports.signAndSendTransaction = signAndSendTransaction // For debugging only. exports.serialize = serialize