diff --git a/config/ganache.json5 b/config/ganache.json5 index e8c91f0b..73441510 100644 --- a/config/ganache.json5 +++ b/config/ganache.json5 @@ -8,6 +8,7 @@ }, rns: { enabled: true, + batchContractAddress: "0xc0b3B62DD0400E4baa721DdEc9B8A384147b23fF", owner: { contractAddress: "0x4bf749ec68270027C5910220CEAB30Cc284c7BA2", eventsEmitter: { diff --git a/config/rskmainnet.json5 b/config/rskmainnet.json5 index 3a3c9134..858d71e9 100644 --- a/config/rskmainnet.json5 +++ b/config/rskmainnet.json5 @@ -3,6 +3,7 @@ enabled: false, }, rns: { + batchContractAddress: "0x2beb2819e110b34c802c761e737470760af2057f", owner: { contractAddress: "0x45d3e4fb311982a06ba52359d44cb4f5980e0ef1", eventsEmitter: { diff --git a/config/rsktestnet.json5 b/config/rsktestnet.json5 index 906537b5..02084a79 100644 --- a/config/rsktestnet.json5 +++ b/config/rsktestnet.json5 @@ -3,6 +3,7 @@ enabled: false, }, rns: { + batchContractAddress: "0x380a47e2a1cec8a21cf00374ea5d092166a702fc", owner: { contractAddress: "0xca0a477e19bac7e0e172ccfd2e3c28a7200bdb71", eventsEmitter: { diff --git a/config/test.json5 b/config/test.json5 index b47a14df..535ddf0a 100644 --- a/config/test.json5 +++ b/config/test.json5 @@ -6,6 +6,7 @@ }, rns: { enabled: true, + batchContractAddress: "0xc0b3b62dd0400e4baa721ddec9b8a384147b23ff", // encoded address used in tests owner: { contractAddress: "OWNER_ADDR" }, diff --git a/package-lock.json b/package-lock.json index 0f7af950..42458210 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@rsksmart/rif-marketplace-cache", - "version": "0.3.0", + "version": "1.0.0-rc.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index df977491..6367dea9 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "pg": "^8.2.1", "pg-hstore": "^2.3.3", "reflect-metadata": "^0.1.13", + "rlp": "^2.2.6", "sequelize": "^5.21.10", "sequelize-store": "^0.3.1", "sequelize-typescript": "^1.1.0", diff --git a/src/services/rns/rns.processor.ts b/src/services/rns/rns.processor.ts index f48677dd..242c8d4e 100644 --- a/src/services/rns/rns.processor.ts +++ b/src/services/rns/rns.processor.ts @@ -12,6 +12,9 @@ import DomainExpiration from './models/expiration.model' import DomainOwner from './models/owner.model' import Transfer from './models/transfer.model' import SoldDomain from './models/sold-domain.model' +import RLP = require('rlp') + +type RLPDecoded = Array> /** * Updates Domain Owner @@ -64,13 +67,48 @@ async function registerTransfer ( return transferDomain.id } +/** + * Decode domain name + * @param decodedData + * @param tokenId + * @param batchAddr + */ +function getDomainName (decodedData: DecodedData, tokenId: string, batchAddr: string): string | undefined { + if (decodedData) { + let name: string | undefined + + if (decodedData.params[0].value === batchAddr) { + // Batch registration + const rlpDecoded: RLPDecoded = RLP.decode(decodedData.params[2].value) as unknown as RLPDecoded + const domainNames = rlpDecoded[1].map( + (domainData: number[]) => + Utils.hexToAscii( + Utils.bytesToHex( + domainData.slice(88, domainData.length) + ) + ) + ) + name = domainNames.find( + (domain: string) => + Utils.numberToHex(Utils.sha3(domain) as string) === tokenId + ) + } else { + // Single registration + const domainData: string = decodedData.params[2].value + name = Utils.hexToAscii('0x' + domainData.slice(218, domainData.length)) + } + + return name + } +} + async function transferHandler (logger: Logger, eventData: EventData, eth: Eth, services: RnsServices): Promise { const tokenId = Utils.numberToHex(eventData.returnValues.tokenId) const ownerAddress = eventData.returnValues.to.toLowerCase() - - const fiftsAddr = config.get('rns.fifsAddrRegistrar.contractAddress') - const registrar = config.get('rns.registrar.contractAddress') - const marketplace = config.get('rns.placement.contractAddress') + const fifsAddr = config.get('rns.fifsAddrRegistrar.contractAddress').toLowerCase() + const registrarAddr = config.get('rns.registrar.contractAddress').toLowerCase() + const marketplaceAddr = config.get('rns.placement.contractAddress').toLowerCase() + const batchAddr = config.get('rns.batchContractAddress').toLowerCase() const tld = config.get('rns.tld') const transactionHash = eventData.transactionHash @@ -83,33 +121,31 @@ async function transferHandler (logger: Logger, eventData: EventData, eth: Eth, const transaction = await eth.getTransaction(transactionHash) const decodedData: DecodedData = abiDecoder.decodeMethod(transaction.input) - if (decodedData) { - const name = Utils.hexToAscii('0x' + decodedData.params[2].value.slice(218, decodedData.params[2].value.length)) + const name: string | undefined = getDomainName(decodedData, tokenId, batchAddr) - if (name) { - try { - await Domain.upsert({ tokenId, name: `${name}.${tld}` }) - } catch (e) { - await Domain.upsert({ tokenId }) - logger.warn(`Domain name ${name}.${tld} for token ${tokenId} could not be stored.`) - } + if (name) { + try { + await Domain.upsert({ tokenId, name: `${name}.${tld}` }) + } catch (e) { + await Domain.upsert({ tokenId }) + logger.warn(`Domain name ${name}.${tld} for token ${tokenId} could not be stored.`) + } - if (domainsService.emit) { - domainsService.emit('patched', { tokenId }) - } + if (domainsService.emit) { + domainsService.emit('patched', { tokenId }) } } } - if (ownerAddress === (fiftsAddr as string).toLowerCase()) { + if (ownerAddress === fifsAddr) { return } - if (ownerAddress === (registrar as string).toLowerCase()) { + if (ownerAddress === registrarAddr) { return } - if (ownerAddress === marketplace.toLowerCase() || from === marketplace.toLowerCase()) { + if (ownerAddress === marketplaceAddr || from === marketplaceAddr) { return // Marketplace transfers are handled in TokenSold } diff --git a/test/services/rns/processor.spec.ts b/test/services/rns/processor.spec.ts index 3fb00c41..c4736164 100644 --- a/test/services/rns/processor.spec.ts +++ b/test/services/rns/processor.spec.ts @@ -20,7 +20,7 @@ import DomainExpiration from '../../../src/services/rns/models/expiration.model' import DomainOffer from '../../../src/services/rns/models/domain-offer.model' import SoldDomain from '../../../src/services/rns/models/sold-domain.model' -import { eventMock } from '../../utils' +import { eventMock, transactionMock } from '../../utils' chai.use(sinonChai) chai.use(chaiAsPromised) @@ -39,10 +39,15 @@ describe('Domain events', () => { const label = 'domain' const name = 'domain.rsk' const tokenId = Utils.sha3(label) as string + const labelOther = 'domainother' + const nameOther = 'domainother.rsk' + const tokenOtherId = Utils.sha3(labelOther) as string const from = 'from_addr' const to = 'to_addr' const other = 'other_addr' const expirationTime = (new Date()).getTime().toString() + const zeroAddress = '0x0000000000000000000000000000000000000000' + const transactionHash = 'TX_HASH' before(() => { sequelize = sequelizeFactory() @@ -78,7 +83,7 @@ describe('Domain events', () => { it('should update Domain Owner on Tranfer', async () => { const event = eventMock({ event: 'Transfer', - transactionHash: 'TX_HASH', + transactionHash, returnValues: { tokenId, from, to } }) @@ -100,12 +105,12 @@ describe('Domain events', () => { it('should handle multiple Transfers in the same transaction', async () => { const event = eventMock({ event: 'Transfer', - transactionHash: 'TX_HASH', + transactionHash, returnValues: { tokenId, from, to } }) const newEvent = eventMock({ event: 'Transfer', - transactionHash: 'TX_HASH', + transactionHash, returnValues: { tokenId, from: to, to: other } }) @@ -237,6 +242,50 @@ describe('Domain events', () => { expect(createdEvent).to.be.instanceOf(Domain) expect(createdEvent?.name).to.be.eql(event.returnValues.name) }) + + it('should Decode Name for new Domain - Batch', async () => { + // Encoded multiple registrations for `domain.rsk` and `domainother.rsk` + const txInput = '0x4000aea0000000000000000000000000c0b3b62dd0400e4baa721ddec9b8a384147b23ff' + + '0000000000000000000000000000000000000000000000003782dace9d90000000000000000000000000000000' + + '000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000' + + '0000000000d2f8d0881bc16d674ec80000f8c5b85ec2c414c890f8bf6a479f320ead074411a4b0e7944ea8c9c1' + + '6d97ebe8ec0de1264229e19b6681ec254dfdb4ee52ed336e6a58d6a635bba4dd00000000000000000000000000' + + '00000000000000000000000000000000000001646f6d61696eb863c2c414c890f8bf6a479f320ead074411a4b0' + + 'e7944ea8c9c17a134ff0de2189b133a610ff8e4a0164c1fd91a7bb3c4052b1fb1bb6792a220800000000000000' + + '00000000000000000000000000000000000000000000000001646f6d61696e6f74686572000000000000000000' + + '0000000000' + + const mockedTransaction = transactionMock(transactionHash, txInput) + eth.getTransaction(transactionHash).resolves(mockedTransaction) + + const event = eventMock({ + event: 'Transfer', + transactionHash, + returnValues: { tokenId, from: zeroAddress, to: to } + }) + + const newEvent = eventMock({ + event: 'Transfer', + transactionHash, + returnValues: { tokenId: tokenOtherId, from: zeroAddress, to: to } + }) + + await processor(event) + + const createdEvent = await Domain.findByPk(Utils.numberToHex(tokenId)) + + expect(createdEvent).to.be.instanceOf(Domain) + expect(createdEvent?.name).to.be.eql(name) + expect(domainServiceEmitSpy).to.have.been.calledWith('patched') + + await processor(newEvent) + + const newCreatedEvent = await Domain.findByPk(Utils.numberToHex(tokenOtherId)) + + expect(newCreatedEvent).to.be.instanceOf(Domain) + expect(newCreatedEvent?.name).to.be.eql(nameOther) + expect(domainServiceEmitSpy).to.have.been.calledWith('patched') + }) }) describe('Offer events', () => { diff --git a/test/utils.ts b/test/utils.ts index 1dd20985..ddc1ddcd 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,4 +1,4 @@ -import { BlockHeader, BlockTransactionString, TransactionReceipt } from 'web3-eth' +import { BlockHeader, BlockTransactionString, TransactionReceipt, Transaction } from 'web3-eth' import { Substitute } from '@fluffy-spoon/substitute' import { EventData } from 'web3-eth-contract' @@ -70,3 +70,16 @@ export function blockMock (blockNumber: number, blockHash = '0x123', options: Pa block.hash.returns!(blockHash) return block } + +export function transactionMock (hash: string, input: string, options: Partial = {}): Transaction { + const transaction = Substitute.for() + + Object.entries(options).forEach(([key, value]) => { + // @ts-ignore + transaction[key].returns!(value) + }) + + transaction.hash.returns!(hash) + transaction.input.returns!(input) + return transaction +}