diff --git a/es/ae/aens.js b/es/ae/aens.js index 9419d5febb..da772755a7 100644 --- a/es/ae/aens.js +++ b/es/ae/aens.js @@ -28,8 +28,9 @@ import * as R from 'ramda' import { encodeBase58Check, salt } from '../utils/crypto' -import { commitmentHash } from '../tx/builder/helpers' +import { commitmentHash, isNameValid } from '../tx/builder/helpers' import Ae from './' +import { CLIENT_TTL, NAME_TTL } from '../tx/builder/schema' /** * Transfer a domain to another account @@ -132,6 +133,7 @@ async function update (nameId, target, options = {}) { * @return {Promise} */ async function query (name, opt = {}) { + isNameValid(name) const o = await this.getName(name) const nameId = o.id @@ -165,6 +167,7 @@ async function query (name, opt = {}) { * @return {Promise} the result of the claim */ async function claim (name, salt, options = {}) { + isNameValid(name) const opt = R.merge(this.Ae.defaults, options) const claimTx = await this.nameClaimTx(R.merge(opt, { accountId: await this.address(opt), @@ -173,11 +176,8 @@ async function claim (name, salt, options = {}) { })) const result = await this.send(claimTx, opt) - - return { - ...result, - ...opt.waitMined && await this.aensQuery(name, opt) - } + const nameInter = this.Chain.defaults.waitMined ? await this.aensQuery(name, opt) : {} + return Object.assign(result, nameInter) } /** @@ -190,6 +190,7 @@ async function claim (name, salt, options = {}) { * @return {Promise} */ async function preclaim (name, options = {}) { + isNameValid(name) const opt = R.merge(this.Ae.defaults, options) const _salt = salt() const height = await this.height() @@ -234,8 +235,8 @@ const Aens = Ae.compose({ deepProps: { Ae: { defaults: { - clientTtl: 1, - nameTtl: 50000 // aec_governance:name_claim_max_expiration() => 50000 + clientTtl: CLIENT_TTL, + nameTtl: NAME_TTL // aec_governance:name_claim_max_expiration() => 50000 } } } diff --git a/es/ae/index.js b/es/ae/index.js index bb74446258..3b12da9dba 100644 --- a/es/ae/index.js +++ b/es/ae/index.js @@ -29,6 +29,8 @@ import Account from '../account' import TxBuilder from '../tx/builder' import * as R from 'ramda' import { BigNumber } from 'bignumber.js' +import { isAddressValid } from '../utils/crypto' +import { isNameValid } from '../tx/builder/helpers' /** * Sign and post a transaction to the chain @@ -63,16 +65,36 @@ async function signUsingGA (tx, options = {}) { * @category async * @rtype (amount: Number|String, recipientId: String, options?: Object) => Promise[String] * @param {Number|String} amount - Amount to spend - * @param {String} recipientId - Address of recipient account + * @param {String} recipientId - Address or Name of recipient account * @param {Object} options - Options * @return {String|String} Transaction or transaction hash */ async function spend (amount, recipientId, options = {}) { const opt = R.merge(this.Ae.defaults, options) - const spendTx = await this.spendTx(R.merge(opt, { senderId: await this.address(opt), recipientId, amount: amount })) + recipientId = await this.resolveRecipientName(recipientId) + const spendTx = await this.spendTx(R.merge(opt, { senderId: await this.address(opt), recipientId, amount })) return this.send(spendTx, opt) } +/** + * Resolve AENS name and return name hash + * + * @param {String} nameOrAddress + * @param {String} pointerPrefix + * @return {String} Address or AENS name hash + */ +async function resolveRecipientName (nameOrAddress) { + if (isAddressValid(nameOrAddress)) return nameOrAddress + if (isNameValid(nameOrAddress)) { + const { id } = await this.getName(nameOrAddress) + return id + } + // Validation + // const { id: nameHash, pointers } = await this.getName(nameOrAddress) + // if (pointers.find(({ id }) => id.split('_')[0] === pointerPrefix)) return nameHash + // throw new Error(`Can't find pointers with prefix ${pointerPrefix} for name ${nameOrAddress}`) +} + /** * Send a percentage of funds to another account * @instance @@ -135,7 +157,7 @@ function destroyInstance () { * @return {Object} Ae instance */ const Ae = stampit(Tx, Account, Chain, { - methods: { send, spend, transferFunds, destroyInstance }, + methods: { send, spend, transferFunds, destroyInstance, resolveRecipientName }, deepProps: { Ae: { defaults: {} } } // Todo Enable GA // deepConfiguration: { Ae: { methods: ['signUsingGA'] } } diff --git a/es/tx/builder/helpers.js b/es/tx/builder/helpers.js index 0d1f6f3c32..dc149c5b1f 100644 --- a/es/tx/builder/helpers.js +++ b/es/tx/builder/helpers.js @@ -1,3 +1,4 @@ +import * as R from 'ramda' import { assertedType, decodeBase58Check, @@ -8,7 +9,7 @@ import { salt } from '../../utils/crypto' import { toBytes } from '../../utils/bytes' -import { ID_TAG_PREFIX, PREFIX_ID_TAG } from './schema' +import { ID_TAG_PREFIX, PREFIX_ID_TAG, AENS_NAME_DOMAINS } from './schema' import { BigNumber } from 'bignumber.js' /** @@ -205,6 +206,20 @@ export function readPointers (pointers) { ) } +/** + * Is name valid + * @function + * @alias module:@aeternity/aepp-sdk/es/ae/aens + * @param {string} name + * @return Boolean + * @throws Error + */ +export function isNameValid (name) { + if (typeof name !== 'string') throw new Error('AENS: Name must be a string') + if (!AENS_NAME_DOMAINS.includes(R.last(name.split('.')))) throw new Error(`AENS: Invalid name domain. Possible domains [${AENS_NAME_DOMAINS}]`) + return true +} + export default { readPointers, buildPointers, @@ -219,5 +234,6 @@ export default { formatSalt, oracleQueryId, createSalt, - buildHash + buildHash, + isNameValid } diff --git a/es/tx/builder/index.js b/es/tx/builder/index.js index beb2635232..1e32e77bfc 100644 --- a/es/tx/builder/index.js +++ b/es/tx/builder/index.js @@ -141,6 +141,10 @@ function validateField (value, key, type, prefix) { return assert((!isNaN(value) || BigNumber.isBigNumber(value)) && BigNumber(value).gte(0), { value, isMinusValue }) } case FIELD_TYPES.id: + if (Array.isArray(prefix)) { + const p = prefix.find(p => p === value.split('_')[0]) + return assert(p && PREFIX_ID_TAG[value.split('_')[0]], { value, prefix }) + } return assert(assertedType(value, prefix) && PREFIX_ID_TAG[value.split('_')[0]] && value.split('_')[0] === prefix, { value, prefix }) case FIELD_TYPES.binary: return assert(value.split('_')[0] === prefix, { prefix, value }) diff --git a/es/tx/builder/schema.js b/es/tx/builder/schema.js index 8ece8f1727..0fdf886c81 100644 --- a/es/tx/builder/schema.js +++ b/es/tx/builder/schema.js @@ -11,6 +11,12 @@ import BigNumber from 'bignumber.js' export const VSN = 1 +export const VSN_2 = 2 + +// # AENS +export const AENS_NAME_DOMAINS = ['aet', 'test'] +export const CLIENT_TTL = 1 +export const NAME_TTL = 50000 // # Tag constant for ids (type uint8) // # see https://github.com/aeternity/protocol/blob/master/serializations.md#the-id-type @@ -374,14 +380,14 @@ const ACCOUNT_TX_2 = [ TX_FIELD('flags', FIELD_TYPES.int), TX_FIELD('nonce', FIELD_TYPES.int), TX_FIELD('balance', FIELD_TYPES.int), - TX_FIELD('gaContract', FIELD_TYPES.id, 'ct'), + TX_FIELD('gaContract', FIELD_TYPES.id, ['ct', 'nm']), TX_FIELD('gaAuthFun', FIELD_TYPES.binary, 'cb') ] const SPEND_TX = [ ...BASE_TX, TX_FIELD('senderId', FIELD_TYPES.id, 'ak'), - TX_FIELD('recipientId', FIELD_TYPES.id, 'ak'), + TX_FIELD('recipientId', FIELD_TYPES.id, ['ak', 'nm']), TX_FIELD('amount', FIELD_TYPES.int), TX_FIELD('fee', FIELD_TYPES.int), TX_FIELD('ttl', FIELD_TYPES.int), @@ -431,7 +437,7 @@ const NAME_TRANSFER_TX = [ TX_FIELD('accountId', FIELD_TYPES.id, 'ak'), TX_FIELD('nonce', FIELD_TYPES.int), TX_FIELD('nameId', FIELD_TYPES.id, 'nm'), - TX_FIELD('recipientId', FIELD_TYPES.id, 'ak'), + TX_FIELD('recipientId', FIELD_TYPES.id, ['ak', 'nm']), TX_FIELD('fee', FIELD_TYPES.int), TX_FIELD('ttl', FIELD_TYPES.int) ] @@ -501,7 +507,7 @@ const CONTRACT_CALL_TX = [ ...BASE_TX, TX_FIELD('callerId', FIELD_TYPES.id, 'ak'), TX_FIELD('nonce', FIELD_TYPES.int), - TX_FIELD('contractId', FIELD_TYPES.id, 'ct'), + TX_FIELD('contractId', FIELD_TYPES.id, ['ct', 'nm']), TX_FIELD('abiVersion', FIELD_TYPES.int), TX_FIELD('fee', FIELD_TYPES.int), TX_FIELD('ttl', FIELD_TYPES.int), @@ -541,7 +547,7 @@ const ORACLE_REGISTER_TX = [ const ORACLE_EXTEND_TX = [ ...BASE_TX, - TX_FIELD('oracleId', FIELD_TYPES.id, 'ok'), + TX_FIELD('oracleId', FIELD_TYPES.id, ['ok', 'nm']), TX_FIELD('nonce', FIELD_TYPES.int), TX_FIELD('oracleTtlType', FIELD_TYPES.int), TX_FIELD('oracleTtlValue', FIELD_TYPES.int), @@ -553,7 +559,7 @@ const ORACLE_QUERY_TX = [ ...BASE_TX, TX_FIELD('senderId', FIELD_TYPES.id, 'ak'), TX_FIELD('nonce', FIELD_TYPES.int), - TX_FIELD('oracleId', FIELD_TYPES.id, 'ok'), + TX_FIELD('oracleId', FIELD_TYPES.id, ['ok', 'nm']), TX_FIELD('query', FIELD_TYPES.string), TX_FIELD('queryFee', FIELD_TYPES.int), TX_FIELD('queryTtlType', FIELD_TYPES.int), diff --git a/test/integration/aens.js b/test/integration/aens.js index f88165b3c2..ce32c929c1 100644 --- a/test/integration/aens.js +++ b/test/integration/aens.js @@ -30,6 +30,7 @@ describe('Aens', function () { configure(this) let aens + let nameHash const account = generateKeyPair() const name = randomName() @@ -68,12 +69,19 @@ describe('Aens', function () { it('updates names', async () => { const claim = await aens.aensQuery(name) + nameHash = claim.id const address = await aens.address() return claim.update(address).should.eventually.deep.include({ pointers: [R.fromPairs([['key', 'account_pubkey'], ['id', address]])] }) }) + it('Spend by name', async () => { + const current = await aens.address() + const onAccount = aens.addresses().find(acc => acc !== current) + await aens.spend(100, name, { onAccount }) + }) + it('transfers names', async () => { const claim = await aens.aensQuery(name)