diff --git a/packages/lib/src/offchainResolver/encoding/decode/decodeAddr.ts b/packages/lib/src/offchainResolver/encoding/decode/decodeAddr.ts new file mode 100644 index 000000000..ca8a4001d --- /dev/null +++ b/packages/lib/src/offchainResolver/encoding/decode/decodeAddr.ts @@ -0,0 +1,21 @@ +import { ethers } from 'ethers'; +import { decodeDnsName } from '../../dns/decodeDnsName'; + +/** +Decodes the call data of addr(bytes 32) +@param ensName - The ENS name to be decoded. +@param data - The data containing the namehash. +@returns An object containing the name. +@throws An error if the namehash doesn't match the ENS name. +*/ +export function decodeAddr(ensName: string, data: ethers.utils.Result) { + const [nameHash] = data; + + const name = decodeDnsName(ensName); + + if (ethers.utils.namehash(name) !== nameHash) { + throw Error("Namehash doesn't match"); + } + + return { name }; +} diff --git a/packages/lib/src/offchainResolver/encoding/decode/decodeText.ts b/packages/lib/src/offchainResolver/encoding/decode/decodeText.ts new file mode 100644 index 000000000..418edec91 --- /dev/null +++ b/packages/lib/src/offchainResolver/encoding/decode/decodeText.ts @@ -0,0 +1,21 @@ +import { ethers } from 'ethers'; +import { decodeDnsName } from '../../dns/decodeDnsName'; + +/** +Decodes the text record of a given ENS name and returns an object containing the name and the record. +@param ensName - The ENS name to be decoded. +@param data - The data containing the namehash and the record. +@returns An object containing the name and the record. +@throws An error if the namehash doesn't match the ENS name. +*/ +export function decodeText(ensName: string, data: ethers.utils.Result) { + const [nameHash, record] = data; + + const name = decodeDnsName(ensName); + + if (ethers.utils.namehash(name) !== nameHash) { + throw Error("Namehash doesn't match"); + } + + return { name, record }; +} diff --git a/packages/lib/src/offchainResolver/encoding/decodeCalldata.test.ts b/packages/lib/src/offchainResolver/encoding/decodeCalldata.test.ts deleted file mode 100644 index 28c7f7fa7..000000000 --- a/packages/lib/src/offchainResolver/encoding/decodeCalldata.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ethers } from 'ethers'; -import { decodeCalldata } from './decodeCalldata'; -import { getResolverInterface } from './getResolverInterface'; -import { encodeEnsName } from '../dns/encodeEnsName'; - -describe('decodeCalldata', () => { - it('decodes valid calldata', () => { - const textData = getResolverInterface().encodeFunctionData('text', [ - ethers.utils.namehash(ethers.utils.nameprep('foo.dm3.eth')), - 'dm3.profile', - ]); - - const calldata = getResolverInterface().encodeFunctionData('resolve', [ - encodeEnsName('foo.dm3.eth'), - textData, - ]); - const { record, name, signature } = decodeCalldata(calldata); - - expect(record).toBe('dm3.profile'); - expect(name).toBe('foo.dm3.eth'); - expect(signature).toBe('text(bytes32,string)'); - }); - - it('throws if namehash does not matched encoded ens.name', () => { - const textData = getResolverInterface().encodeFunctionData('text', [ - ethers.utils.namehash(ethers.utils.nameprep('FOOO')), - 'dm3.profile', - ]); - - const calldata = getResolverInterface().encodeFunctionData('resolve', [ - encodeEnsName('foo.dm3.eth'), - textData, - ]); - - expect(() => decodeCalldata(calldata)).toThrowError( - "Namehash doesn't match", - ); - }); -}); diff --git a/packages/lib/src/offchainResolver/encoding/decodeCalldata.ts b/packages/lib/src/offchainResolver/encoding/decodeCalldata.ts deleted file mode 100644 index cb613664b..000000000 --- a/packages/lib/src/offchainResolver/encoding/decodeCalldata.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ethers } from 'ethers'; -import { log } from '../../shared/log'; -import { decodeDnsName } from '../dns/decodeDnsName'; -import { DecodedCcipRequest } from '../types'; -import { getResolverInterface } from './getResolverInterface'; -/** - * This function can be used to decode calldata return by the resolve method of the Offchain Resolver Smart Contract - * This encoded calldata must have the following format - * "resolve (decodeDnsName(name),text(namehash(name),record))"" - * To find out how to build the calldata you may check out {@see encodeFunctionData} {@see getResolverInterface} - * or the unit test provided in this package - * @param calldata the encoded calldata string - * @returns {@see DecodedCcipRequest} - */ - -export function decodeCalldata(calldata: string): DecodedCcipRequest { - try { - const textResolver = getResolverInterface(); - - //Parse the calldata returned by a contra - const [rawName, data] = textResolver.parseTransaction({ - data: calldata, - }).args; - - //The name has to be normalized before be it can be processed - const encodedName = ethers.utils.nameprep(rawName); - - const { signature, args } = textResolver.parseTransaction({ - data, - }); - const [nameHash, record] = args; - - const name = decodeDnsName(encodedName); - - if (ethers.utils.namehash(name) !== nameHash) { - throw Error("Namehash doesn't match"); - } - - return { name, record, signature }; - } catch (err: any) { - log("[Decode Calldata] Can't decode calldata "); - log(err); - throw err; - } -} diff --git a/packages/lib/src/offchainResolver/encoding/decodeRequest.test.ts b/packages/lib/src/offchainResolver/encoding/decodeRequest.test.ts new file mode 100644 index 000000000..79c1dc4c9 --- /dev/null +++ b/packages/lib/src/offchainResolver/encoding/decodeRequest.test.ts @@ -0,0 +1,68 @@ +import { ethers } from 'ethers'; +import { decodeRequest } from './decodeRequest'; +import { getResolverInterface } from './getResolverInterface'; +import { encodeEnsName } from '../dns/encodeEnsName'; + +describe('decodeRequest', () => { + describe('decodeText', () => { + it('decodes valid calldata', () => { + const textData = getResolverInterface().encodeFunctionData('text', [ + ethers.utils.namehash('foo.dm3.eth'), + 'dm3.profile', + ]); + + const calldata = getResolverInterface().encodeFunctionData( + 'resolve', + [encodeEnsName('foo.dm3.eth'), textData], + ); + const { request } = decodeRequest(calldata); + + expect(request.record).toBe('dm3.profile'); + expect(request.name).toBe('foo.dm3.eth'); + }); + + it('throws if namehash does not matched encoded ens.name', () => { + const textData = getResolverInterface().encodeFunctionData('text', [ + ethers.utils.namehash('FOOO'), + 'dm3.profile', + ]); + + const calldata = getResolverInterface().encodeFunctionData( + 'resolve', + [encodeEnsName('foo.dm3.eth'), textData], + ); + + expect(() => decodeRequest(calldata)).toThrowError( + "Namehash doesn't match", + ); + }); + }); + describe('decodeAddr', () => { + it('decodes valid calldata', () => { + const textData = getResolverInterface().encodeFunctionData('addr', [ + ethers.utils.namehash('foo.dm3.eth'), + ]); + + const calldata = getResolverInterface().encodeFunctionData( + 'resolve', + [encodeEnsName('foo.dm3.eth'), textData], + ); + const { request } = decodeRequest(calldata); + expect(request.name).toBe('foo.dm3.eth'); + }); + it('throws if namehash does not matched encoded ens.name', () => { + const textData = getResolverInterface().encodeFunctionData('addr', [ + ethers.utils.namehash('FOOO'), + ]); + + const calldata = getResolverInterface().encodeFunctionData( + 'resolve', + [encodeEnsName('foo.dm3.eth'), textData], + ); + + expect(() => decodeRequest(calldata)).toThrowError( + "Namehash doesn't match", + ); + }); + }); +}); diff --git a/packages/lib/src/offchainResolver/encoding/decodeRequest.ts b/packages/lib/src/offchainResolver/encoding/decodeRequest.ts new file mode 100644 index 000000000..85a13c63f --- /dev/null +++ b/packages/lib/src/offchainResolver/encoding/decodeRequest.ts @@ -0,0 +1,39 @@ +import { log } from '../../shared/log'; +import { DecodedCcipRequest } from '../types'; +import { decodeAddr } from './decode/decodeAddr'; +import { decodeText } from './decode/decodeText'; +import { getResolverInterface } from './getResolverInterface'; + +/** +Decodes a given calldata string and returns a DecodedCcipRequest object containing the signature and request. +@param calldata - The calldata string to be decoded. +@returns A {@see DecodedCcipRequest} object containing the signature and request. +@throws An error if the calldata cannot be decoded or if the signature is not supported. +*/ +export function decodeRequest(calldata: string): DecodedCcipRequest { + try { + const textResolver = getResolverInterface(); + + //Parse the calldata returned by a contra + const [ensName, data] = textResolver.parseTransaction({ + data: calldata, + }).args; + + const { signature, args } = textResolver.parseTransaction({ + data, + }); + + switch (signature) { + case 'text(bytes32,string)': + return { signature, request: decodeText(ensName, args) }; + case 'addr(bytes32)': + return { signature, request: decodeAddr(ensName, args) }; + default: + throw Error(`${signature} is not supported`); + } + } catch (err: any) { + log("[Decode Calldata] Can't decode calldata "); + log(err); + throw err; + } +} diff --git a/packages/lib/src/offchainResolver/encoding/encodeUserProfile.test.ts b/packages/lib/src/offchainResolver/encoding/encodeResponse.test.ts similarity index 82% rename from packages/lib/src/offchainResolver/encoding/encodeUserProfile.test.ts rename to packages/lib/src/offchainResolver/encoding/encodeResponse.test.ts index cd08eb9d3..b52fd34ed 100644 --- a/packages/lib/src/offchainResolver/encoding/encodeUserProfile.test.ts +++ b/packages/lib/src/offchainResolver/encoding/encodeResponse.test.ts @@ -1,13 +1,13 @@ import { ethers } from 'ethers'; import { UserProfile } from '../../account'; import { encodeEnsName } from '../dns/encodeEnsName'; -import { encodeUserProfile } from './encodeUserProfile'; +import { encodeResponse } from './encodeResponse'; import { getResolverInterface } from './getResolverInterface'; -describe('encodeUserProfie', () => { +describe('encodeResponse', () => { it('encodes userProfile properly', async () => { const signer = ethers.Wallet.createRandom(); - const profile: UserProfile = { + const response: UserProfile = { publicSigningKey: '0ekgI3CBw2iXNXudRdBQHiOaMpG9bvq9Jse26dButug=', publicEncryptionKey: 'Vrd/eTAk/jZb/w5L408yDjOO5upNFDGdt0lyWRjfBEk=', deliveryServices: [''], @@ -25,10 +25,10 @@ describe('encodeUserProfie', () => { const functionSelector = 'text(bytes32,string)'; - const encodedProfile = await encodeUserProfile( + const encodedProfile = await encodeResponse( signer, - profile, signer.address, + response, calldata, functionSelector, ); @@ -42,6 +42,6 @@ describe('encodeUserProfie', () => { 'text', encodedResult, ); - expect(JSON.parse(decodedProfile)).toStrictEqual(profile); + expect(JSON.parse(decodedProfile)).toStrictEqual(response); }); }); diff --git a/packages/lib/src/offchainResolver/encoding/encodeUserProfile.ts b/packages/lib/src/offchainResolver/encoding/encodeResponse.ts similarity index 95% rename from packages/lib/src/offchainResolver/encoding/encodeUserProfile.ts rename to packages/lib/src/offchainResolver/encoding/encodeResponse.ts index fd75ba28c..860518eea 100644 --- a/packages/lib/src/offchainResolver/encoding/encodeUserProfile.ts +++ b/packages/lib/src/offchainResolver/encoding/encodeResponse.ts @@ -13,10 +13,10 @@ import { getResolverInterface } from './getResolverInterface'; * @param ttl the time to life to calculate validUntil. * @returns the encoded response */ -export async function encodeUserProfile( +export async function encodeResponse( signer: Signer, - userProfile: UserProfile, resolverAddr: string, + response: any, request: string, functionSelector: string, ttl: number = 300, @@ -25,7 +25,7 @@ export async function encodeUserProfile( const validUntil = Math.floor(Date.now() / 1000 + ttl); const result = textResolver.encodeFunctionResult(functionSelector, [ - stringify(userProfile), + stringify(response), ]); /** * This hash has to be compiled the same way as at the OffchainResolver.makeSignatureHash method diff --git a/packages/lib/src/offchainResolver/encoding/getResolverInterface.ts b/packages/lib/src/offchainResolver/encoding/getResolverInterface.ts index e2992c69a..da2e05e7b 100644 --- a/packages/lib/src/offchainResolver/encoding/getResolverInterface.ts +++ b/packages/lib/src/offchainResolver/encoding/getResolverInterface.ts @@ -6,5 +6,6 @@ export function getResolverInterface() { 'function text(bytes32 node, string calldata key) external view returns (string memory)', // eslint-disable-next-line max-len 'function resolveWithProof(bytes calldata response, bytes calldata extraData) external view returns (bytes memory)', + 'function addr(bytes32 node) external view returns (address)', ]); } diff --git a/packages/lib/src/offchainResolver/index.ts b/packages/lib/src/offchainResolver/index.ts index e6932e4cf..e57171e62 100644 --- a/packages/lib/src/offchainResolver/index.ts +++ b/packages/lib/src/offchainResolver/index.ts @@ -1,5 +1,5 @@ -export { decodeCalldata } from './encoding/decodeCalldata'; -export { encodeUserProfile } from './encoding/encodeUserProfile'; +export { decodeRequest } from './encoding/decodeRequest'; +export { encodeResponse } from './encoding/encodeResponse'; export { decodeDnsName } from './dns/decodeDnsName'; export { encodeEnsName } from './dns/encodeEnsName'; diff --git a/packages/lib/src/offchainResolver/types.ts b/packages/lib/src/offchainResolver/types.ts index efa2de665..b8f757d62 100644 --- a/packages/lib/src/offchainResolver/types.ts +++ b/packages/lib/src/offchainResolver/types.ts @@ -1,10 +1,8 @@ /** - * @param name the ENS name after after decoding - * @param record the name of the record that should be queried + * @param request the decoded function params of the request. i.E {name:string,record:string} * @param signature the signature of the function the request should query */ export interface DecodedCcipRequest { - name: string; - record: string; + request: any; signature: string; } diff --git a/packages/offchain-resolver/src/http/ccipGateway.test.ts b/packages/offchain-resolver/src/http/ccipGateway.test.ts index 7b617d917..ece38f715 100644 --- a/packages/offchain-resolver/src/http/ccipGateway.test.ts +++ b/packages/offchain-resolver/src/http/ccipGateway.test.ts @@ -67,177 +67,204 @@ describe('CCIP Gateway', () => { }); describe('Get UserProfile Offchain', () => { - it('Returns valid Offchain profile', async () => { - const { signer, profile, signature } = await getSignedUserProfile(); - - await dm3User.sendTransaction({ - to: signer, - value: hreEthers.BigNumber.from(1), - }); - - const name = 'foo.dm3.eth'; - - //Create the profile in the first place - const writeRes = await request(profileApp).post(`/name`).send({ - name, - address: signer, - signedUserProfile: { - profile, - signature, - }, - }); - - expect(writeRes.status).to.equal(200); - //Call the contract to retrieve the gateway url - const { callData, sender } = await resolveGateWayUrl( - name, - offchainResolver, - ); - - //You the url returned by he contract to fetch the profile from the ccip gateway - const { body, status } = await request(ccipApp) - .get(`/${sender}/${callData}`) - .send(); + describe('ResolveText', () => { + it('Returns valid Offchain profile', async () => { + const { signer, profile, signature } = + await getSignedUserProfile(); + + await dm3User.sendTransaction({ + to: signer, + value: hreEthers.BigNumber.from(1), + }); + + const name = 'foo.dm3.eth'; + + //Create the profile in the first place + const writeRes = await request(profileApp).post(`/name`).send({ + name, + address: signer, + signedUserProfile: { + profile, + signature, + }, + }); + + expect(writeRes.status).to.equal(200); + //Call the contract to retrieve the gateway url + const { callData, sender } = await resolveGateWayUrl( + name, + offchainResolver, + ); - expect(status).to.equal(200); + //You the url returned by he contract to fetch the profile from the ccip gateway + const { body, status } = await request(ccipApp) + .get(`/${sender}/${callData}`) + .send(); - const resultString = await offchainResolver.resolveWithProof( - body.data, - callData, - ); + expect(status).to.equal(200); - const [actualProfile] = getResolverInterface().decodeFunctionResult( - 'text', - resultString, - ); + const resultString = await offchainResolver.resolveWithProof( + body.data, + callData, + ); - expect(JSON.parse(actualProfile)).to.eql(profile); - }); - it('Returns 404 if profile does not exists', async () => { - const { signer, profile, signature } = await getSignedUserProfile(); + const [actualProfile] = + getResolverInterface().decodeFunctionResult( + 'text', + resultString, + ); - const name = 'foo.dm3.eth'; + expect(JSON.parse(actualProfile)).to.eql(profile); + }); + it('Returns 404 if profile does not exists', async () => { + const { signer, profile, signature } = + await getSignedUserProfile(); - //Call the contract to retrieve the gateway url - const { callData, sender } = await resolveGateWayUrl( - name, - offchainResolver, - ); + const name = 'foo.dm3.eth'; - //You the url returned by he contract to fetch the profile from the ccip gateway - const { body, status } = await request(ccipApp) - .get(`/${sender}/${callData}`) - .send(); + //Call the contract to retrieve the gateway url + const { callData, sender } = await resolveGateWayUrl( + name, + offchainResolver, + ); - expect(status).to.equal(404); - }); - it('Returns 400 if record is not dm3.profile', async () => { - //Call the contract to retrieve the gateway url - const resolveGatewayUrlForTheWrongRecord = async () => { - try { - const textData = getResolverInterface().encodeFunctionData( - 'text', - [ - ethers.utils.namehash( - ethers.utils.nameprep('foo.dm3.eth'), - ), - 'unknown.record', - ], - ); + //You the url returned by he contract to fetch the profile from the ccip gateway + const { body, status } = await request(ccipApp) + .get(`/${sender}/${callData}`) + .send(); - //This always revers and throws the OffchainLookup Exceptions hence we need to catch it - await offchainResolver.resolve( - Lib.offchainResolver.encodeEnsName('foo.dm3.eth'), - textData, - ); - return { - gatewayUrl: '', - callbackFunction: '', - extraData: '', - }; - } catch (err: any) { - const { sender, urls, callData } = err.errorArgs; - //Decode call - - //Replace template vars - const gatewayUrl = urls[0] - .replace('{sender}', sender) - .replace('{data}', callData); - - return { gatewayUrl, sender, callData }; - } - }; - const { sender, callData } = - await resolveGatewayUrlForTheWrongRecord(); - //You the url returned by he contract to fetch the profile from the ccip gateway - const { status } = await request(ccipApp) - .get(`/${sender}/${callData}`) - .send(); - - expect(status).to.equal(400); - }); - it('Returns 400 if something failed during the request', async () => { - //You the url returned by he contract to fetch the profile from the ccip gateway - const { body, status } = await request(ccipApp) - .get(`/foo/bar`) - .send(); - - expect(status).to.equal(400); - expect(body.message).to.equal('Unknown error'); + expect(status).to.equal(404); + }); + it('Returns 400 if record is not dm3.profile', async () => { + //Call the contract to retrieve the gateway url + const resolveGatewayUrlForTheWrongRecord = async () => { + try { + const textData = + getResolverInterface().encodeFunctionData('text', [ + ethers.utils.namehash('foo.dm3.eth'), + 'unknown.record', + ]); + + //This always revers and throws the OffchainLookup Exceptions hence we need to catch it + await offchainResolver.resolve( + Lib.offchainResolver.encodeEnsName('foo.dm3.eth'), + textData, + ); + return { + gatewayUrl: '', + callbackFunction: '', + extraData: '', + }; + } catch (err: any) { + const { sender, urls, callData } = err.errorArgs; + //Decode call + + //Replace template vars + const gatewayUrl = urls[0] + .replace('{sender}', sender) + .replace('{data}', callData); + + return { gatewayUrl, sender, callData }; + } + }; + const { sender, callData } = + await resolveGatewayUrlForTheWrongRecord(); + //You the url returned by he contract to fetch the profile from the ccip gateway + const { status } = await request(ccipApp) + .get(`/${sender}/${callData}`) + .send(); + + expect(status).to.equal(400); + }); + it('Returns 400 if something failed during the request', async () => { + //You the url returned by he contract to fetch the profile from the ccip gateway + const { body, status } = await request(ccipApp) + .get(`/foo/bar`) + .send(); + + expect(status).to.equal(400); + expect(body.message).to.equal('Unknown error'); + }); }); }); describe('E2e test', () => { - it('resolves propfile using ethers.provider.getText()', async () => { - const { signer, profile, signature } = await getSignedUserProfile(); - await dm3User.sendTransaction({ - to: signer, - value: hreEthers.BigNumber.from(1), - }); - - const name = 'foo.dm3.eth'; + describe('resolveText', () => { + it('resolves propfile using ethers.provider.getText()', async () => { + const { signer, profile, signature } = + await getSignedUserProfile(); + await dm3User.sendTransaction({ + to: signer, + value: hreEthers.BigNumber.from(1), + }); + + const name = 'foo.dm3.eth'; + + //Create the profile in the first place + const writeRes = await request(profileApp).post(`/name`).send({ + name, + address: signer, + signedUserProfile: { + profile, + signature, + }, + }); + expect(writeRes.status).to.equal(200); + + const provider = new MockProvider( + hreEthers.provider, + fetchProfileFromCcipGateway, + offchainResolver, + ); - //Create the profile in the first place - const writeRes = await request(profileApp).post(`/name`).send({ - name, - address: signer, - signedUserProfile: { - profile, - signature, - }, - }); - expect(writeRes.status).to.equal(200); + const resolver = await provider.getResolver('foo.dm3.eth'); - const provider = new MockProvider( - hreEthers.provider, - fetchProfileFromCcipGateway, - offchainResolver, - ); + const text = await resolver.getText('dm3.profile'); - const resolver = new ethers.providers.Resolver( - provider, - offchainResolver.address, - 'foo.dm3.eth', - ); + expect(JSON.parse(text)).to.eql(profile); + }); + it('Throws error if lookup went wrong', async () => { + const provider = new MockProvider( + hreEthers.provider, + fetchProfileFromCcipGateway, + offchainResolver, + ); - const text = await resolver.getText('dm3.profile'); + const resolver = await provider.getResolver('foo.dm3.eth'); - expect(JSON.parse(text)).to.eql(profile); + expect(resolver.getText('unknown record')).rejected; + }); }); - it('Throws error if lookup went wrong', async () => { - const provider = new MockProvider( - hreEthers.provider, - fetchProfileFromCcipGateway, - offchainResolver, - ); - - const resolver = new ethers.providers.Resolver( - provider, - offchainResolver.address, - 'foo.dm3.eth', - ); + describe('ResolveAddr', () => { + it('resolvesName returns the Address of the name', async () => { + const { signer, profile, signature } = + await getSignedUserProfile(); + await dm3User.sendTransaction({ + to: signer, + value: hreEthers.BigNumber.from(1), + }); + + const name = 'foo.dm3.eth'; + + //Create the profile in the first place + const writeRes = await request(profileApp).post(`/name`).send({ + name, + address: signer, + signedUserProfile: { + profile, + signature, + }, + }); + expect(writeRes.status).to.equal(200); + + const provider = new MockProvider( + hreEthers.provider, + fetchProfileFromCcipGateway, + offchainResolver, + ); - expect(resolver.getText('unknown record')).rejected; + await provider.resolveName('foo.dm3.eth'); + }); }); }); const fetchProfileFromCcipGateway = async (url: string, json?: string) => { @@ -274,6 +301,13 @@ describe('CCIP Gateway', () => { this.fetcher = fetcher; this.offchainResolver = offchainResolver; } + async getResolver(name: string) { + return new ethers.providers.Resolver( + this, + offchainResolver.address, + name, + ) as any; + } async perform(method: string, params: any): Promise { switch (method) { @@ -390,7 +424,7 @@ const resolveGateWayUrl = async ( ) => { try { const textData = getResolverInterface().encodeFunctionData('text', [ - ethers.utils.namehash(ethers.utils.nameprep(ensName)), + ethers.utils.namehash(ensName), 'dm3.profile', ]); diff --git a/packages/offchain-resolver/src/http/ccipGateway.ts b/packages/offchain-resolver/src/http/ccipGateway.ts index 607c083b5..6c7ffec3e 100644 --- a/packages/offchain-resolver/src/http/ccipGateway.ts +++ b/packages/offchain-resolver/src/http/ccipGateway.ts @@ -1,7 +1,7 @@ import * as Lib from 'dm3-lib/dist.backend'; -import ethers from 'ethers'; import { Signer } from 'ethers'; import express from 'express'; +import { handleCcipRequest } from './handleCcipRequest/handleCcipRequest'; import { WithLocals } from './types'; export function ccipGateway(signer: Signer, resolverAddr: string) { @@ -14,32 +14,23 @@ export function ccipGateway(signer: Signer, resolverAddr: string) { res: express.Response, ) => { const { resolverAddr, calldata } = req.params; - //TODO should we blackist all request that are not orignated from our dm3 resolver? CC@Heiko try { - const { record, name, signature } = - Lib.offchainResolver.decodeCalldata(calldata); + const { request, signature } = + Lib.offchainResolver.decodeRequest(calldata); - if (record !== 'dm3.profile') { - return res.status(400).send({ - message: `Record is not supported by this resolver`, - }); - } - - //Todo get rid of unused onchain userprofile - const profile = await req.app.locals.db.getUserProfile(name); - - if (!profile) { - return res - .status(404) - .send({ message: 'Profile not found' }); - } + const response = await handleCcipRequest( + req, + res, + signature, + request, + ); - const data = await Lib.offchainResolver.encodeUserProfile( + const data = await Lib.offchainResolver.encodeResponse( signer, - profile, resolverAddr, + response, calldata, - signature, + 'text(bytes32,string)', ); return res.send({ data }); diff --git a/packages/offchain-resolver/src/http/handleCcipRequest/handleCcipRequest.ts b/packages/offchain-resolver/src/http/handleCcipRequest/handleCcipRequest.ts new file mode 100644 index 000000000..0c7ab41a3 --- /dev/null +++ b/packages/offchain-resolver/src/http/handleCcipRequest/handleCcipRequest.ts @@ -0,0 +1,19 @@ +import express from 'express'; +import { handleAddr } from './handler/handleAddr'; +import { handleText } from './handler/resolveText'; +export async function handleCcipRequest( + req: express.Request, + res: express.Response, + signature: string, + request: any, +) { + switch (signature) { + case 'text(bytes32,string)': + return await handleText(res, req.app.locals.db, request); + case 'addr(bytes32)': + return await handleAddr(res, req.app.locals.db, request); + + default: + res.status(400).send({ message: `${signature} is not supported` }); + } +} diff --git a/packages/offchain-resolver/src/http/handleCcipRequest/handler/handleAddr.ts b/packages/offchain-resolver/src/http/handleCcipRequest/handler/handleAddr.ts new file mode 100644 index 000000000..ec2629732 --- /dev/null +++ b/packages/offchain-resolver/src/http/handleCcipRequest/handler/handleAddr.ts @@ -0,0 +1,17 @@ +import { IDatabase } from '../../../persistance/IDatabase'; +import express from 'express'; +import { ethers } from 'ethers'; + +export async function handleAddr( + res: express.Response, + db: IDatabase, + request: any, +) { + const { name } = request; + + const addr = await db.getAddressByName(ethers.utils.namehash(name)); + if (!addr) { + return res.status(404).send({ message: 'Name not found' }); + } + return addr; +} diff --git a/packages/offchain-resolver/src/http/handleCcipRequest/handler/resolveText.ts b/packages/offchain-resolver/src/http/handleCcipRequest/handler/resolveText.ts new file mode 100644 index 000000000..2411d51c9 --- /dev/null +++ b/packages/offchain-resolver/src/http/handleCcipRequest/handler/resolveText.ts @@ -0,0 +1,22 @@ +import express from 'express'; +import { IDatabase } from '../../../persistance/IDatabase'; + +export async function handleText( + res: express.Response, + db: IDatabase, + request: any, +) { + const { record, name } = request; + if (record !== 'dm3.profile') { + return res.status(400).send({ + message: `Record is not supported by this resolver`, + }); + } + + const response = await db.getUserProfile(name); + + if (!response) { + return res.status(404).send({ message: 'Profile not found' }); + } + return response; +} diff --git a/packages/offchain-resolver/src/http/profile.test.ts b/packages/offchain-resolver/src/http/profile.test.ts index a37240149..99851d3eb 100644 --- a/packages/offchain-resolver/src/http/profile.test.ts +++ b/packages/offchain-resolver/src/http/profile.test.ts @@ -5,9 +5,9 @@ import request from 'supertest'; import { getDatabase, getRedisClient, Redis } from '../persistance/getDatabase'; import { IDatabase } from '../persistance/IDatabase'; import { profile } from './profile'; -const SENDER_ADDRESS = '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292'; import * as Lib from 'dm3-lib/dist.backend'; const { expect } = require('chai'); +const SENDER_ADDRESS = '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292'; describe('Profile', () => { let redisClient: Redis; diff --git a/packages/offchain-resolver/src/persistance/IDatabase.ts b/packages/offchain-resolver/src/persistance/IDatabase.ts index 9d132281a..00d5d6ed9 100644 --- a/packages/offchain-resolver/src/persistance/IDatabase.ts +++ b/packages/offchain-resolver/src/persistance/IDatabase.ts @@ -11,4 +11,5 @@ export interface IDatabase { address: string, ): Promise; hasAddressProfile(name: string): Promise; + getAddressByName(nameHash: string): Promise; } diff --git a/packages/offchain-resolver/src/persistance/getDatabase.ts b/packages/offchain-resolver/src/persistance/getDatabase.ts index e79070077..8735ead0e 100644 --- a/packages/offchain-resolver/src/persistance/getDatabase.ts +++ b/packages/offchain-resolver/src/persistance/getDatabase.ts @@ -6,9 +6,10 @@ export async function getDatabase(_redis?: Redis): Promise { return { getUserProfile: Profile.getUserProfile(redis), - getUserProfileByAddress: Profile.getUserProfileByAddress(redis), setUserProfile: Profile.setUserProfile(redis), + getUserProfileByAddress: Profile.getUserProfileByAddress(redis), hasAddressProfile: Profile.hasAddressProfile(redis), + getAddressByName: Profile.getAddressByName(redis), }; } diff --git a/packages/offchain-resolver/src/persistance/profile/getAddressByName.ts b/packages/offchain-resolver/src/persistance/profile/getAddressByName.ts new file mode 100644 index 000000000..918da5fb1 --- /dev/null +++ b/packages/offchain-resolver/src/persistance/profile/getAddressByName.ts @@ -0,0 +1,13 @@ +import { NAME_TO_ADDRESS_KEY } from '.'; +import { Redis } from '../getDatabase'; + +export function getAddressByName(redis: Redis) { + return async (nameHash: string) => { + const isMember = await redis.exists(NAME_TO_ADDRESS_KEY + nameHash); + if (!isMember) { + return null; + } + + return await redis.get(NAME_TO_ADDRESS_KEY + nameHash); + }; +} diff --git a/packages/offchain-resolver/src/persistance/profile/getUserProfile.ts b/packages/offchain-resolver/src/persistance/profile/getUserProfile.ts index 232539bd1..6b21a4796 100644 --- a/packages/offchain-resolver/src/persistance/profile/getUserProfile.ts +++ b/packages/offchain-resolver/src/persistance/profile/getUserProfile.ts @@ -12,7 +12,7 @@ import { ethers } from 'ethers'; */ export function getUserProfile(redis: Redis) { return async (name: string) => { - const nameHash = ethers.utils.namehash(ethers.utils.nameprep(name)); + const nameHash = ethers.utils.namehash(name); const isMember = await redis.hExists(USER_PROFILE_KEY, nameHash); if (!isMember) { diff --git a/packages/offchain-resolver/src/persistance/profile/index.ts b/packages/offchain-resolver/src/persistance/profile/index.ts index dca93dd64..22bfbb33b 100644 --- a/packages/offchain-resolver/src/persistance/profile/index.ts +++ b/packages/offchain-resolver/src/persistance/profile/index.ts @@ -2,5 +2,8 @@ export { getUserProfile } from './getUserProfile'; export { setUserProfile } from './setUserProfile'; export { hasAddressProfile } from './hasAddressProfile'; export { getUserProfileByAddress } from './getUserProfileByAddress'; +export { getAddressByName } from './getAddressByName'; + export const USER_PROFILE_KEY = 'USER_PROFILE:'; export const ADDRESS_TO_PROFILE_KEY = 'ADDRESS_TO_PROFILE_KEY:'; +export const NAME_TO_ADDRESS_KEY = 'NAME_TO_ADDRESS_KEY:'; diff --git a/packages/offchain-resolver/src/persistance/profile/setUserProfile.ts b/packages/offchain-resolver/src/persistance/profile/setUserProfile.ts index 2b3166136..1bc2d4260 100644 --- a/packages/offchain-resolver/src/persistance/profile/setUserProfile.ts +++ b/packages/offchain-resolver/src/persistance/profile/setUserProfile.ts @@ -1,6 +1,10 @@ import * as Lib from 'dm3-lib/dist.backend'; import { Redis } from '../getDatabase'; -import { ADDRESS_TO_PROFILE_KEY, USER_PROFILE_KEY } from '.'; +import { + ADDRESS_TO_PROFILE_KEY, + NAME_TO_ADDRESS_KEY, + USER_PROFILE_KEY, +} from '.'; import { ethers } from 'ethers'; /** @@ -26,7 +30,7 @@ export function setUserProfile(redis: Redis) { throw Error('Invalid user profile'); } - const nameHash = ethers.utils.namehash(ethers.utils.nameprep(name)); + const nameHash = ethers.utils.namehash(name); const writeResult = await redis.hSet( USER_PROFILE_KEY, @@ -35,6 +39,7 @@ export function setUserProfile(redis: Redis) { ); await redis.set(ADDRESS_TO_PROFILE_KEY + address, nameHash); + await redis.set(NAME_TO_ADDRESS_KEY + nameHash, address); return !!writeResult; };