diff --git a/package.json b/package.json index 0c06558..6c01b69 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,9 @@ "@ethvault/iframe-provider": "0.1.9", "base58check": "^2.0.0", "bech32": "^1.1.3", + "bns": "^0.14.0", "bs58": "^4.0.1", + "bufio": "^1.0.7", "cross-fetch": "^3.0.2", "dns-packet": "^5.2.1", "eth-ens-namehash": "^2.0.8", diff --git a/src/contracts.js b/src/contracts.js index d557ac8..76e8c9b 100644 --- a/src/contracts.js +++ b/src/contracts.js @@ -3,6 +3,7 @@ import { abi as ensContract } from '@ensdomains/contracts/abis/ens/ENS.json' import { abi as reverseRegistrarContract } from '@ensdomains/contracts/abis/ens/ReverseRegistrar.json' import { abi as oldResolverContract } from '@ensdomains/contracts/abis/ens-022/PublicResolver.json' import { abi as resolverContract } from '@ensdomains/contracts/abis/resolver/Resolver.json' +import { abi as dnsResolverContract } from '@ensdomains/contracts/abis/resolver/DNSResolver.json' import { abi as testRegistrarContract } from '@ensdomains/contracts/abis/ens/TestRegistrar.json' import { abi as dnsRegistrarContract } from '@ensdomains/contracts/abis/dnsregistrar/DNSRegistrar.json' import { abi as legacyAuctionRegistrarContract } from '@ensdomains/contracts/abis/ens/HashRegistrar' @@ -19,6 +20,10 @@ function getResolverContract({ address, provider }) { return new Contract(address, resolverContract, provider) } +function getDNSResolverContract({ address, provider }) { + return new Contract(address, dnsResolverContract, provider) +} + function getOldResolverContract({ address, provider }) { return new Contract(address, oldResolverContract, provider) } @@ -60,6 +65,7 @@ export { getReverseRegistrarContract, getENSContract, getResolverContract, + getDNSResolverContract, getOldResolverContract, getDnsRegistrarContract, getPermanentRegistrarContract, diff --git a/src/ens.js b/src/ens.js index a49e82d..b88897f 100644 --- a/src/ens.js +++ b/src/ens.js @@ -10,7 +10,8 @@ import { import { normalize } from 'eth-ens-namehash' import { formatsByName } from '@ensdomains/address-encoder' import { abi as ensContract } from '@ensdomains/contracts/abis/ens/ENS.json' - +import bns from 'bns' +import {BufferReader, BufferWriter} from 'bufio' import { decryptHashes } from './preimage' import { @@ -30,6 +31,7 @@ import { getReverseRegistrarContract, getENSContract, getResolverContract, + getDNSResolverContract, getOldResolverContract } from './contracts' @@ -54,6 +56,58 @@ function getLabelhash(label) { return labelhash(label) } +function hashDNSName(name) { + const dnsName = bns.encoding.packName(bns.util.fqdn(name)); + return utils.keccak256(dnsName); +} + +// Parse a DNS record in text format and writes to a buffer +// Accepts rrs with empty rdata (ttl will always be 0 if rdata is empty): +// name.eth. 200 IN A 127.0.0.1 +// name.eth. 0 IN A +function writeDNSRecordFromZone(bw, rr) { + try { + const record = bns.wire.Record.fromString(rr); + bw.writeBytes(record.encode()); + } catch (e) { + // this record may have empty rdata + // [owner_name] [ttl] [class] [type] [rdata] + const parts = rr.trim().split(/[\s]+/) + if (parts.length !== 4) { + // doesn't seem like a valid record + throw new Error('unable to parse record'); + } + + const rname = parts[0]; + if(!bns.util.isFQDN(rname)) { + throw new Error('owner name must be a fully qualified domain name'); + } + + const rtype = bns.wire.stringToType(parts[3]); + const rclass = bns.wire.stringToClass(parts[2]); + + // manually encode the record + // since bns errors on empty rdata + bw.writeBytes(bns.encoding.packName(rname)); + bw.writeU16BE(rtype); + bw.writeU16BE(rclass); + bw.writeU32BE(0); // zero TTL + bw.writeU16BE(0); // zero RDLENGTH + } +} + +function decodeDNSRecords(data) { + const br = new BufferReader(data) + const records = [] + + while(br.left() > 0) { + let str = bns.wire.Record.read(br).toString() + records.push(str) + } + + return records +} + export class ENS { constructor({ networkId, registryAddress, provider }) { this.registryAddress = registryAddress @@ -228,6 +282,42 @@ export class ENS { } } + async getDNSRecordsZoneFormat(nodeName, dnsName, dnsType) { + const data = await this.getRawDNSRecords(nodeName, dnsName, dnsType); + return decodeDNSRecords(data); + } + + async getRawDNSRecords(nodeName, dnsName, dnsType) { + const resolverAddr = await this.getResolver(nodeName) + return this.getRawDNSRecordsWithResolver(nodeName, dnsName, dnsType, resolverAddr) + } + + async getRawDNSRecordsWithResolver(nodeName, dnsName, dnsType, resolverAddr) { + if (parseInt(resolverAddr, 16) === 0) { + return [] + } + + const type = bns.wire.stringToType(dnsType) + const namehash = getNamehash(bns.util.trimFQDN(nodeName)) + + try { + const provider = await getProvider() + const Resolver = getDNSResolverContract({ + address: resolverAddr, + provider + }) + + const data = await Resolver.dnsRecord(namehash, hashDNSName(dnsName), type) + return Buffer.from(data.substr(2), 'hex') + } catch (e) { + console.warn( + 'Error getting dns record on the dns resolver contract, are you sure the resolver address is a resolver contract?', e + ) + return [] + } + + } + async getName(address) { const reverseNode = `${address.slice(2)}.addr.reverse` const resolverAddr = await this.getResolver(reverseNode) @@ -492,6 +582,46 @@ export class ENS { return Resolver.setText(namehash, key, recordValue) } + // Sets DNS records from an array of rrs in zone format + // + // rrs example: [ + // hello.eth. 300 IN A 127.0.0.1 + // hello.eth. 300 IN A 127.0.0.2 + // hello.eth. 300 IN TXT + // _443._tcp.hello.eth. 300 IN TLSA 3 1 1 [HASH] + // ] + // + // This will: + // 1. Add two A records + // 2. Remove any TXT records from hello.eth. + // 3. Add a TLSA record + async setDNSRecordsFromZone(name, rrs) { + const bw = new BufferWriter(); + for (const rr of rrs) { + writeDNSRecordFromZone(bw, rr); + } + + const data = bw.render(); + return this.setRawDNSRecords(name, data); + } + + async setRawDNSRecords(name, data) { + const resolverAddr = await this.getResolver(name) + return this.setRawDNSRecordsWithResolver(name, data, resolverAddr) + } + + async setRawDNSRecordsWithResolver(name, data, resolverAddr) { + const namehash = getNamehash(name) + const provider = await getProvider() + const ResolverWithoutSigner = getDNSResolverContract({ + address: resolverAddr, + provider + }) + const signer = await getSigner() + const Resolver = ResolverWithoutSigner.connect(signer) + return Resolver.setDNSRecords(namehash, data) + } + async createSubdomain(name) { const account = await getAccount() const publicResolverAddress = process.env.REACT_APP_TLD_RESOLVER || diff --git a/yarn.lock b/yarn.lock index c9b97f7..28fb07a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2607,11 +2607,31 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +bcrypto@~5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/bcrypto/-/bcrypto-5.4.0.tgz#4046f0c44a4b301eff84de593b4f86fce8d91db2" + integrity sha512-KDX2CR29o6ZoqpQndcCxFZAtYA1jDMnXU3jmCfzP44g++Cu7AHHtZN/JbrN/MXAg9SLvtQ8XISG+eVD9zH1+Jg== + dependencies: + bufio "~1.0.7" + loady "~0.0.5" + bech32@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.3.tgz#bd47a8986bbb3eec34a56a097a84b8d3e9a2dfcd" integrity sha512-yuVFUvrNcoJi0sv5phmqc6P+Fl1HjRDRNOOkHY2X/3LBy2bIGNSFx4fZ95HMaXHupuS7cZR15AsvtmCIF4UEyg== +bfile@~0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/bfile/-/bfile-0.2.2.tgz#b0c205cee1ff22a9525304ec51f09195a7afa077" + integrity sha512-X205SsJ7zFAnjeJ/pBLqDqF10x/4Su3pBy8UdVKw4hdGJk7t5pLoRi+uG4rPaDAClGbrEfT/06PGUbYiMYKzTg== + +bheep@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/bheep/-/bheep-0.1.5.tgz#ed6a3da7857c8ebe71d71de9571ddd8f42340889" + integrity sha512-0KR5Zi8hgJBKL35+aYzndCTtgSGakOMxrYw2uszd5UmXTIfx3+drPGoETlVbQ6arTdAzSoQYA1j35vbaWpQXBg== + dependencies: + bsert "~0.0.10" + big-integer@1.6.36: version "1.6.36" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.36.tgz#78631076265d4ae3555c04f85e7d9d2f3a071a36" @@ -2644,6 +2664,14 @@ bindings@^1.2.1, bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" +binet@~0.3.6: + version "0.3.6" + resolved "https://registry.yarnpkg.com/binet/-/binet-0.3.6.tgz#82d9462fa601759956a1f1f1f0f10a5760445f85" + integrity sha512-6pm+Gc3uNiiJZEv0k8JDWqQlo9ki/o9UNAkLmr0EGm7hI5MboOJVIOlO1nw3YuDkLHWN78OPsaC4JhRkn2jMLw== + dependencies: + bs32 "~0.1.5" + bsert "~0.0.10" + bip39@2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/bip39/-/bip39-2.5.0.tgz#51cbd5179460504a63ea3c000db3f787ca051235" @@ -2695,6 +2723,23 @@ bn.js@^4.11.9: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== +bns@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/bns/-/bns-0.14.0.tgz#538d0d91ec8272c32c774b82827699f2db168173" + integrity sha512-lqxDpj9gX7OtihwAzMhlMk0w38ObZu+aRSF9fzG/wUfF6RfocFSFDGHA+KVbLNXudiTStzhWVGc8cJmcL4IHYw== + dependencies: + bcrypto "~5.4.0" + bfile "~0.2.2" + bheep "~0.1.5" + binet "~0.3.6" + bs32 "~0.1.6" + bsert "~0.0.10" + btcp "~0.1.5" + budp "~0.1.6" + bufio "~1.0.7" + optionalDependencies: + unbound "~0.4.3" + body-parser@1.19.0, body-parser@^1.16.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" @@ -2838,6 +2883,13 @@ browserslist@^4.6.0, browserslist@^4.7.2: electron-to-chromium "^1.3.295" node-releases "^1.1.38" +bs32@~0.1.5, bs32@~0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/bs32/-/bs32-0.1.6.tgz#2710cb70da3f55447138181fa645e2c54b1ef6d0" + integrity sha512-usjDesQqZ8ihHXOnOEQuAdymBHnJEfSd+aELFSg1jN/V3iAf12HrylHlRJwIt6DTMmXpBDQ+YBg3Q3DIYdhRgQ== + dependencies: + bsert "~0.0.10" + bs58@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/bs58/-/bs58-2.0.1.tgz#55908d58f1982aba2008fa1bed8f91998a29bf8d" @@ -2873,6 +2925,21 @@ bser@^2.0.0: dependencies: node-int64 "^0.4.0" +bsert@~0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/bsert/-/bsert-0.0.10.tgz#231ac82873a1418c6ade301ab5cd9ae385895597" + integrity sha512-NHNwlac+WPy4t2LoNh8pXk8uaIGH3NSaIUbTTRXGpE2WEbq0te/tDykYHkFK57YKLPjv/aGHmbqvnGeVWDz57Q== + +btcp@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/btcp/-/btcp-0.1.5.tgz#f76262415a0f6eaa592cdbb91c8b18386530ac28" + integrity sha512-tkrtMDxeJorn5p0KxaLXELneT8AbfZMpOFeoKYZ5qCCMMSluNuwut7pGccLC5YOJqmuk0DR774vNVQLC9sNq/A== + +budp@~0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/budp/-/budp-0.1.6.tgz#bbe166cc4842cf8d96754ee29afb1af56f6757c8" + integrity sha512-o+a8NPq3DhV91j4nInjht2md6mbU1XL+7ciPltP66rw5uD3KP1m5r8lA94LZVaPKcFdJ0l2HVVzRNxnY26Pefg== + buffer-alloc-unsafe@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" @@ -2947,6 +3014,11 @@ bufferutil@^4.0.1: dependencies: node-gyp-build "^4.2.0" +bufio@^1.0.7, bufio@~1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/bufio/-/bufio-1.0.7.tgz#b7f63a1369a0829ed64cc14edf0573b3e382a33e" + integrity sha512-bd1dDQhiC+bEbEfg56IdBv7faWa6OipMs/AFFFvtFnB3wAYjlwQpQRZ0pm6ZkgtfL0pILRXhKxOiQj6UzoMR7A== + bytes@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" @@ -7313,6 +7385,11 @@ load-json-file@^4.0.0: pify "^3.0.0" strip-bom "^3.0.0" +loady@~0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/loady/-/loady-0.0.5.tgz#b17adb52d2fb7e743f107b0928ba0b591da5d881" + integrity sha512-uxKD2HIj042/HBx77NBcmEPsD+hxCgAtjEWlYNScuUjIsh/62Uyu39GOR68TBR68v+jqDL9zfftCWoUo4y03sQ== + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -10415,6 +10492,13 @@ ultron@~1.1.0: resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og== +unbound@~0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/unbound/-/unbound-0.4.3.tgz#ca4e9a3c8b678df431ec576385acc8bff9aa439a" + integrity sha512-2ISqZLXtzp1l9f1V8Yr6S+zuhXxEwE1CjKHjXULFDHJcfhc9Gm3mn19hdPp4rlNGEdCivKYGKjYe3WRGnafYdA== + dependencies: + loady "~0.0.5" + unbzip2-stream@^1.0.9: version "1.3.3" resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz#d156d205e670d8d8c393e1c02ebd506422873f6a"