diff --git a/app.json b/app.json index 5a38786..5d7af8a 100644 --- a/app.json +++ b/app.json @@ -16,7 +16,7 @@ "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#000000" }, - "versionCode": 7 + "versionCode": 10 }, "owner": "sns-manager" } diff --git a/eas.json b/eas.json index dd6afff..a89f97b 100644 --- a/eas.json +++ b/eas.json @@ -22,6 +22,18 @@ "SOLANA_RPC_URL": "https://helius-proxy.bonfida.com" } }, + "solana-store": { + "distribution": "store", + "node": "18.15.0", + "autoIncrement": false, + "android": { + "buildType": "apk" + }, + "env": { + "SOLANA_RPC_URL": "https://helius-proxy.bonfida.com" + } + }, + "production": { "extends": "base", "distribution": "store", diff --git a/package-lock.json b/package-lock.json index 89b273c..3f7d6a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,9 @@ "@bonfida/name-offers": "0.0.10", "@bonfida/name-tokenizer": "0.0.12", "@bonfida/sns-emitter": "0.1.7", - "@bonfida/sns-react": "2.0.2", - "@bonfida/spl-name-service": "2.0.3", + "@bonfida/sns-react": "2.0.3", + "@bonfida/sns-records": "0.0.1-alpha.8", + "@bonfida/spl-name-service": "2.0.4", "@coral-xyz/common-public": "0.2.0-latest.3375", "@expo-google-fonts/dev": "*", "@expo/html-elements": "0.5.1", @@ -2344,11 +2345,11 @@ } }, "node_modules/@bonfida/sns-react": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@bonfida/sns-react/-/sns-react-2.0.2.tgz", - "integrity": "sha512-JHWofh9Xq6vftQB/zBt+COr4ck74UzW/F9tJnAnGo8d7GZLqg22w0oc8/yzZnIj/91/QCO3jY21iQatXyDDstg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@bonfida/sns-react/-/sns-react-2.0.3.tgz", + "integrity": "sha512-DltSEAR6/eWCDRnuZc0rPsJgd6cFXHEQXmrRfE3WKop64O/h8YrvCmUjyBSXygt9x3TejHFkXlp/yTTHpsEMqw==", "dependencies": { - "@bonfida/spl-name-service": "^1.6.1", + "@bonfida/spl-name-service": "2.0.4", "@solana/web3.js": "^1.87.6", "react-async-hook": "^4.0.0" }, @@ -2357,53 +2358,6 @@ "react-dom": ">=16.8" } }, - "node_modules/@bonfida/sns-react/node_modules/@bonfida/spl-name-service": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@bonfida/spl-name-service/-/spl-name-service-1.6.1.tgz", - "integrity": "sha512-AWIPJM6uoofz2tCDRdD8wf1kCzs32yHYmRRQSIzYJ3HuIDufqoilxn8svxsvKRQA2Fm2up6kqnesROp2aoVMCA==", - "dependencies": { - "@ethersproject/sha2": "^5.7.0", - "@pythnetwork/client": "^2.19.0", - "@solana/buffer-layout": "^4.0.1", - "@solana/spl-token": "0.3.7 ", - "bech32-buffer": "^0.2.1", - "bn.js": "^5.2.1", - "borsh": "^0.7.0", - "buffer": "^6.0.3", - "ipaddr.js": "^2.1.0", - "punycode": "^2.3.0", - "tweetnacl": "^1.0.3" - }, - "peerDependencies": { - "@solana/web3.js": "^1.75.0" - } - }, - "node_modules/@bonfida/sns-react/node_modules/@solana/spl-token": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.7.tgz", - "integrity": "sha512-bKGxWTtIw6VDdCBngjtsGlKGLSmiu/8ghSt/IOYJV24BsymRbgq7r12GToeetpxmPaZYLddKwAz7+EwprLfkfg==", - "dependencies": { - "@solana/buffer-layout": "^4.0.0", - "@solana/buffer-layout-utils": "^0.2.0", - "buffer": "^6.0.3" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "@solana/web3.js": "^1.47.4" - } - }, - "node_modules/@bonfida/sns-react/node_modules/borsh": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", - "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", - "dependencies": { - "bn.js": "^5.2.0", - "bs58": "^4.0.0", - "text-encoding-utf-8": "^1.0.2" - } - }, "node_modules/@bonfida/sns-records": { "version": "0.0.1-alpha.8", "resolved": "https://registry.npmjs.org/@bonfida/sns-records/-/sns-records-0.0.1-alpha.8.tgz", @@ -2496,9 +2450,9 @@ "integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ==" }, "node_modules/@bonfida/spl-name-service": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@bonfida/spl-name-service/-/spl-name-service-2.0.3.tgz", - "integrity": "sha512-AXQQ0RrUelblqtoajjMfpPKMDnP1mPPaYmvTn/BeEjOhQwwM9sy7RCHX21D9a2eYlZMA08gYr52TY3y01VQz4w==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@bonfida/spl-name-service/-/spl-name-service-2.0.4.tgz", + "integrity": "sha512-JZsuK6E6T0ufqfrl/mmaxkkfyyFKcbKn6StvA2BcIf9VBJcObbDhM3nzOSVBtI6LwxWhuPMG6KdtKSA9EJZMeg==", "dependencies": { "@bonfida/sns-records": "0.0.1-alpha.8", "@ethersproject/hash": "^5.7.0", diff --git a/package.json b/package.json index 2aff763..8c1cac8 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "build:mobile:preview:local": "eas build -p android --profile preview --local", "build:mobile:prod": "eas build -p android --profile production", "build:mobile:prod:local": "eas build -p android --profile production --local", + "build:mobile:prod:solana-store": "eas build -p android --profile solana-store", "start": "npm run build:xnt && npx xnft web", "bundle": "npm run build:xnft && npx xnft bundle", "dev": "GENERATE_SOURCEMAP=false expo start --clear --web & npx xnft --iframe http://localhost:19006", @@ -27,8 +28,9 @@ "@bonfida/name-offers": "0.0.10", "@bonfida/name-tokenizer": "0.0.12", "@bonfida/sns-emitter": "0.1.7", - "@bonfida/sns-react": "2.0.2", - "@bonfida/spl-name-service": "2.0.3", + "@bonfida/sns-react": "2.0.3", + "@bonfida/sns-records": "0.0.1-alpha.8", + "@bonfida/spl-name-service": "2.0.4", "@coral-xyz/common-public": "0.2.0-latest.3375", "@expo-google-fonts/dev": "*", "@expo/html-elements": "0.5.1", diff --git a/publishing/config.yaml b/publishing/config.yaml index 6408c3a..188393f 100644 --- a/publishing/config.yaml +++ b/publishing/config.yaml @@ -19,7 +19,7 @@ app: - purpose: icon uri: ../assets/dapp-store/fida-icon-512x512.png release: - address: ELyJgn72Kxn6gMSk46eTREP6MVi82JLbuowRA8XBG6Kp + address: 3hLUYLTWWa3uktaCq4NcYRfJK9LBqa73wiVr4PWzmbuo media: - purpose: icon uri: ../assets/dapp-store/fida-icon-512x512.png @@ -35,7 +35,7 @@ release: uri: ../assets/dapp-store/screenshots/cart-payment-1080x1920.png files: - purpose: install - uri: build-1696412787473.apk + uri: ../application-b76ef173-b792-4037-8d3b-98ef6ba99ea6.apk catalog: en-US: name: SNS Manager diff --git a/publishing/package.json b/publishing/package.json index db91ab2..0b39df4 100644 --- a/publishing/package.json +++ b/publishing/package.json @@ -10,6 +10,6 @@ "author": "", "license": "ISC", "devDependencies": { - "@solana-mobile/dapp-store-cli": "^0.5.2" + "@solana-mobile/dapp-store-cli": "^0.6.0" } } diff --git a/publishing/pnpm-lock.yaml b/publishing/pnpm-lock.yaml index a7aec94..182f3ab 100644 --- a/publishing/pnpm-lock.yaml +++ b/publishing/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: devDependencies: '@solana-mobile/dapp-store-cli': - specifier: ^0.5.2 - version: 0.5.2 + specifier: ^0.6.0 + version: 0.6.0 packages: @@ -575,6 +575,7 @@ packages: /@bundlr-network/client@0.8.9(debug@4.3.4): resolution: {integrity: sha512-SJ7BAt/KhONeFQ0+nbqrw2DUWrsev6y6cmlXt+3x7fPCkw7OJwudtxV/h2nBteZd65NXjqw8yzkmLiLfZ7CCRA==} + deprecated: Bundlr is now Irys - please switch to @irys/sdk - this package will remain compatible with Irys for the foreseeable future. hasBin: true dependencies: '@solana/wallet-adapter-base': 0.9.23(@solana/web3.js@1.68.0) @@ -1616,14 +1617,14 @@ packages: tslib: 2.6.2 dev: true - /@solana-mobile/dapp-store-cli@0.5.2: - resolution: {integrity: sha512-PkevK+bHMNfVuSwO/mBFCSjV7dIu0sBIhD4CNhDyEEdBBSaRzlvOo0mYkwSDvyTIbfN1IHVtmAIzZFJXoA9UZg==} + /@solana-mobile/dapp-store-cli@0.6.0: + resolution: {integrity: sha512-17pVza46mKqplkpIFZ0tUo+gD/mDN/ZmDqBoaGv5KSYErxTzuqDWNHc9yjSgBNur2VHdwwNmdMDNKD81tW2ksw==} engines: {node: '>=18'} hasBin: true dependencies: '@aws-sdk/client-s3': 3.405.0 '@metaplex-foundation/js-plugin-aws': 0.18.3 - '@solana-mobile/dapp-store-publishing-tools': 0.5.2 + '@solana-mobile/dapp-store-publishing-tools': 0.6.0 '@solana/web3.js': 1.68.0 '@types/semver': 7.5.1 ajv: 8.12.0 @@ -1649,8 +1650,8 @@ packages: - utf-8-validate dev: true - /@solana-mobile/dapp-store-publishing-tools@0.5.2: - resolution: {integrity: sha512-rzRELWoQOtVAobfpTSVdCh4p42N6XrSbVka2dAbQhj1uex0RJiQQsEvx+EcdtRLNeJuLZup84gv4t5Z10p56CA==} + /@solana-mobile/dapp-store-publishing-tools@0.6.0: + resolution: {integrity: sha512-EBXrWEq+vCcaYE63T5Q3110LuNo3+7dh1pZQ001tTvFHcAbupBMjsy0liI5t7HtheUW3TwMJ19B7v76pKpGZxA==} engines: {node: '>=18'} dependencies: '@metaplex-foundation/js': 0.18.3 @@ -1910,7 +1911,7 @@ packages: algosdk: 1.24.1 arweave: 1.14.4 arweave-stream-tx: 1.2.2(arweave@1.14.4) - avsc: github.com/Bundlr-Network/avsc/a730cc8018b79e114b6a3381bbb57760a24c6cef + avsc: git@github.com+Bundlr-Network/avsc/a730cc8018b79e114b6a3381bbb57760a24c6cef axios: 0.21.4(debug@4.3.4) base64url: 3.0.1 bs58: 4.0.1 @@ -3893,8 +3894,8 @@ packages: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: true - github.com/Bundlr-Network/avsc/a730cc8018b79e114b6a3381bbb57760a24c6cef: - resolution: {tarball: https://codeload.github.com/Bundlr-Network/avsc/tar.gz/a730cc8018b79e114b6a3381bbb57760a24c6cef} + git@github.com+Bundlr-Network/avsc/a730cc8018b79e114b6a3381bbb57760a24c6cef: + resolution: {commit: a730cc8018b79e114b6a3381bbb57760a24c6cef, repo: git@github.com:Bundlr-Network/avsc.git, type: git} name: avsc version: 5.4.7 engines: {node: '>=0.11'} diff --git a/src/App.tsx b/src/App.tsx index 8b73233..4a28d0c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -47,6 +47,7 @@ import { NavigatorTabsParamList } from "@src/types"; import { LanguageHeader } from "@src/components/Header"; import { SolanaProvider } from "@src/providers/SolanaProvider"; import ErrorBoundary from "react-native-error-boundary"; +import { RecordV2BadgeExplanationModal } from "./components/RecordV2Badge"; const xnftjson = require("../xnft.json"); @@ -65,6 +66,7 @@ const modalConfig = { DomainSizeModal: DomainSizeModal, LanguageModal: LanguageModal, TokenizeModal: TokenizeModal, + RecordV2BadgeExplanationModal: RecordV2BadgeExplanationModal, }; const stackModal = createModalStack(modalConfig); diff --git a/src/components/EditPicture.tsx b/src/components/EditPicture.tsx index 9281823..cc42083 100644 --- a/src/components/EditPicture.tsx +++ b/src/components/EditPicture.tsx @@ -5,13 +5,11 @@ import * as ImagePicker from "expo-image-picker"; import { Record, getDomainKeySync, - NameRegistryState, - transferInstruction, - NAME_PROGRAM_ID, - createNameRegistry, - updateInstruction, - Numberu32, NAME_OFFERS_ID, + getRecordV2Key, + createRecordV2Instruction, + updateRecordV2Instruction, + validateRecordV2Content, } from "@bonfida/spl-name-service"; import { registerFavourite } from "@bonfida/name-offers"; import { isTokenized } from "@bonfida/name-tokenizer"; @@ -19,7 +17,6 @@ import { t } from "@lingui/macro"; import { PublicKey, TransactionInstruction } from "@solana/web3.js"; import { useStatusModalContext } from "@src/contexts/StatusModalContext"; import { useSolanaConnection } from "@src/hooks/xnft-hooks"; -import { removeZeroRight } from "@src/utils/record/zero"; import { sendTx } from "@src/utils/send-tx"; import { WrapModal } from "./WrapModal"; import { unwrap } from "@src/utils/unwrap"; @@ -51,10 +48,7 @@ export const EditPicture = ({ try { setLoading(true); const ixs: TransactionInstruction[] = []; - const { pubkey, parent } = getDomainKeySync( - Record.Pic + "." + domain, - true, - ); + const recordKey = getRecordV2Key(domain, Record.Pic); try { new URL(pic); @@ -81,67 +75,37 @@ export const EditPicture = ({ } // Check if exists - const info = await connection.getAccountInfo(pubkey); + const info = await connection.getAccountInfo(recordKey); if (!info?.data) { - const space = 2_000; - const lamports = await connection.getMinimumBalanceForRentExemption( - space + NameRegistryState.HEADER_LEN, - ); - const ix = await createNameRegistry( - connection, - Buffer.from([1]).toString() + Record.Pic, - space, // Hardcode space to 2kB - new PublicKey(publicKey), - new PublicKey(publicKey), - lamports, - undefined, - parent, + const ix = createRecordV2Instruction( + domain, + Record.Pic, + pic, + publicKey, + publicKey, ); ixs.push(ix); } else { - // Zero the data stored - const { registry } = await NameRegistryState.retrieve( - connection, - pubkey, + const ix = updateRecordV2Instruction( + domain, + Record.Pic, + pic, + publicKey, + publicKey, ); - - if (!registry.owner.equals(new PublicKey(publicKey))) { - // Record was created before domain was transfered - const ix = transferInstruction( - NAME_PROGRAM_ID, - pubkey, - new PublicKey(publicKey), - registry.owner, - undefined, - parent, - new PublicKey(publicKey), - ); - ixs.push(ix); - } - - if (registry.data) { - const trimmed = removeZeroRight(registry.data); - const zero = Buffer.alloc(trimmed.length); - const ix = updateInstruction( - NAME_PROGRAM_ID, - pubkey, - new Numberu32(0), - zero, - new PublicKey(publicKey), - ); - ixs.push(ix); - } + ixs.push(ix); } - const data = Buffer.from(pic, "utf-8"); - const ix = updateInstruction( - NAME_PROGRAM_ID, - pubkey, - new Numberu32(0), - data, - new PublicKey(publicKey), + ixs.push( + validateRecordV2Content( + true, + domain, + Record.Pic, + publicKey, + publicKey, + publicKey, + ), ); - ixs.push(ix); const sig = await sendTx( connection, diff --git a/src/components/ProfileBlock.tsx b/src/components/ProfileBlock.tsx index 5ee9d85..bf134b3 100644 --- a/src/components/ProfileBlock.tsx +++ b/src/components/ProfileBlock.tsx @@ -4,19 +4,19 @@ import Clipboard from "@react-native-clipboard/clipboard"; import { t } from "@lingui/macro"; import { LinearGradient } from "expo-linear-gradient"; import { Feather, FontAwesome } from "@expo/vector-icons"; -import { useProfilePic } from "@bonfida/sns-react"; import { useModal } from "react-native-modalfy"; import { useStatusModalContext } from "@src/contexts/StatusModalContext"; import tw from "@src/utils/tailwind"; import { abbreviate } from "@src/utils/abbreviate"; import { useWallet } from "@src/hooks/useWallet"; import { useFavoriteDomain } from "@src/hooks/useFavoriteDomain"; +import { usePicRecord } from "@src/hooks/useRecords"; interface ProfileBlockProps { children?: ReactNode; owner: string; domain: string; - picRecord: ReturnType; + picRecord: ReturnType; } export const ProfileBlock = ({ @@ -52,8 +52,10 @@ export const ProfileBlock = ({ > openModal("EditPicture", { - currentPic: picRecord.result, + currentPic: picRecord.uri, domain: domain, setAsFav: !favorite.result?.reverse, + refresh: picRecord.execute, }) } style={tw`h-[24px] w-[24px] rounded-full flex items-center justify-center absolute bottom-0 right-0 bg-brand-accent`} diff --git a/src/components/RecordV2Badge.tsx b/src/components/RecordV2Badge.tsx new file mode 100644 index 0000000..e62fdfc --- /dev/null +++ b/src/components/RecordV2Badge.tsx @@ -0,0 +1,130 @@ +import { GUARDIANS, Record, SELF_SIGNED } from "@bonfida/spl-name-service"; +import { + CheckCircleIcon, + ExclamationTriangleIcon, + InformationCircleIcon, + ShieldCheckIcon, + ShieldExclamationIcon, +} from "react-native-heroicons/solid"; +import { WrapModal } from "./WrapModal"; +import { Text, TouchableOpacity, View } from "react-native"; +import tw from "@src/utils/tailwind"; +import { useModal } from "react-native-modalfy"; +import { t } from "@lingui/macro"; +import { UiButton } from "./UiButton"; + +export const RecordV2BadgeExplanationModal = ({ + modal: { closeModal, getParam }, +}: { + modal: { closeModal: () => void; getParam: (a: string, b?: string) => T }; +}) => { + const text = getParam("text"); + const title = getParam("title"); + + return ( + + {text} + + closeModal()} /> + + ); +}; + +export const StaleBadge = ({ stale }: { stale: boolean }) => { + const { openModal } = useModal(); + if (stale) { + return ( + + openModal("RecordV2BadgeExplanationModal", { + text: t`This record is not signed`, + title: t`Record staleness`, + }) + } + > + + + ); + } + return ( + + openModal("RecordV2BadgeExplanationModal", { + text: t`Signed by the domain owner`, + title: t`Record staleness`, + }) + } + > + + + ); +}; + +export const RoaBadge = ({ record, roa }: { record: Record; roa: boolean }) => { + const { openModal } = useModal(); + const roaSupported = SELF_SIGNED.has(record) || GUARDIANS.has(record); + + if (!roaSupported) { + return ( + + openModal("RecordV2BadgeExplanationModal", { + text: t`Record content ownership is not supported for this type of record`, + title: t`Record Right of Association`, + }) + } + > + + + ); + } + + if (!roa) { + return ( + + openModal("RecordV2BadgeExplanationModal", { + text: t`Record content ownership is not verified`, + title: t`Record Right of Association`, + }) + } + > + + + ); + } + + return ( + + openModal("RecordV2BadgeExplanationModal", { + text: t`Ownership is verified`, + title: t`Record Right of Association`, + }) + } + > + + + ); +}; + +export const RecordV2Badge = ({ + record, + recordDefined, + roa, + stale, +}: { + record: Record; + recordDefined: boolean; + roa: boolean; + stale: boolean; +}) => { + if (!recordDefined) return null; + + return ( + + + + + ); +}; diff --git a/src/hooks/useRecords.tsx b/src/hooks/useRecords.tsx index cace38c..e39814f 100644 --- a/src/hooks/useRecords.tsx +++ b/src/hooks/useRecords.tsx @@ -1,6 +1,16 @@ import { useSolanaConnection } from "../hooks/xnft-hooks"; -import { Record } from "@bonfida/spl-name-service"; -import { useDeserializedRecords } from "@bonfida/sns-react"; +import { + ETH_ROA_RECORDS, + GUARDIANS, + Record, + RecordResult, + SELF_SIGNED, +} from "@bonfida/spl-name-service"; +import { useRecordsV2 } from "@bonfida/sns-react"; +import { Validation } from "@bonfida/sns-records"; +import { useCallback, useEffect, useState } from "react"; +import { PublicKey } from "@solana/web3.js"; +import { useDomainInfo } from "./useDomainInfo"; export type AddressRecord = | Record.BSC @@ -8,7 +18,6 @@ export type AddressRecord = | Record.DOGE | Record.ETH | Record.LTC - // | Record.SOL | Record.Injective; export type SocialRecord = @@ -42,33 +51,154 @@ export const ADDRESS_RECORDS: AddressRecord[] = [ Record.DOGE, Record.ETH, Record.LTC, - // Record.SOL, Record.Injective, ]; +interface Result { + record: Record; + roa: boolean; + stale: boolean; + deserialized: string | undefined; +} + +type ExecuteFunction = () => Promise; + +interface Data { + result: Result[]; + execute: ExecuteFunction; + loading: boolean; +} + export const useRecords = (domain: string | undefined, records: Record[]) => { const connection = useSolanaConnection(); - const res = useDeserializedRecords(connection!, domain || "", records); - const { result, ...rest } = res; - - return { - result: result?.map((e, idx) => { - return { record: records[idx], value: e }; - }), - ...rest, - }; -}; + const res = useRecordsV2(connection!, domain || "", records, true); + const domainInfo = useDomainInfo(domain!); -export const useSocialRecords = (domain: string) => { - return useRecords(domain, SOCIAL_RECORDS); -}; + const execute: ExecuteFunction = useCallback(async () => { + try { + await domainInfo.execute(); + await res.execute(); + } catch (error) { + console.error(error); + } + }, [domainInfo, res]); + + const [data, setData] = useState>({ + result: [], + execute, + loading: true, + }); + + useEffect(() => { + if (!res || !res.result || !domainInfo || !domainInfo.result) { + return setData({ + execute, + result: [], + loading: res.loading || domainInfo.loading, + }); + } + + const _data = []; + for (let i = 0; i < records.length; i++) { + let r = res.result[i]; + // By default assume it's stale and no RoA + let stale = true; + let roa = false; + + const header = r?.retrievedRecord.header; + const stalenessId = r?.retrievedRecord.getStalenessId(); + const roaId = r?.retrievedRecord.getRoAId(); + const owner = new PublicKey(domainInfo.result.owner); -export const useAddressRecords = (domain: string) => { - return useRecords(domain, ADDRESS_RECORDS); + // Check staleness + if ( + stalenessId?.equals(owner.toBuffer()) && + header?.stalenessValidation === Validation.Solana + ) { + stale = false; + } + + // Check RoA + const validation = ETH_ROA_RECORDS.has(r?.record!) + ? Validation.Ethereum + : Validation.Solana; + const selfSigned = SELF_SIGNED.has(r?.record!); + const verifier = selfSigned + ? r?.retrievedRecord.getContent() + : GUARDIANS.get(r?.record!)?.toBuffer(); + + if ( + verifier && + roaId?.equals(verifier) && + header?.rightOfAssociationValidation === validation + ) { + roa = true; + } + _data.push({ + deserialized: r?.deserializedContent, + roa, + stale, + record: records[i], + }); + } + setData({ + result: _data, + execute, + loading: domainInfo.loading || res.loading, + }); + }, [ + res.loading, + JSON.stringify(res.result?.map((e) => e?.deserializedContent)), + domainInfo.result?.owner, + domainInfo.result?.isTokenized, + domain, + ...records, + domainInfo.loading, + ]); + + return data; }; +export const createUseRecords = (records: Record[]) => (domain: string) => + useRecords(domain, records); + +export const useSocialRecords = createUseRecords(SOCIAL_RECORDS); +export const useAddressRecords = createUseRecords(ADDRESS_RECORDS); + +export interface PicRecord { + uri: string | undefined; + loading: boolean; + execute: ExecuteFunction; +} + export const usePicRecord = (domain: string | undefined) => { - const { result, loading, execute } = useRecords(domain, [Record.Pic]); - const des = result ? result[0] : undefined; - return { pic: des, loading, execute }; + const { + result, + execute: executeRecords, + loading, + } = useRecords(domain, [Record.Pic]); + const execute: ExecuteFunction = useCallback(async () => { + try { + await executeRecords(); + } catch (error) { + console.error(error); + } + }, [loading, domain]); + + const [data, setData] = useState({ + uri: undefined, + loading: true, + execute: execute, + }); + + useEffect(() => { + if (result[0]?.deserialized && !result[0]?.stale) { + setData({ uri: result[0].deserialized, loading: false, execute }); + } + if (!loading) { + setData((prev) => ({ ...prev, loading: false })); + } + }, [loading, domain]); + + return data; }; diff --git a/src/hooks/useRecordsV2Guardians/index.tsx b/src/hooks/useRecordsV2Guardians/index.tsx new file mode 100644 index 0000000..f2f9db5 --- /dev/null +++ b/src/hooks/useRecordsV2Guardians/index.tsx @@ -0,0 +1,11 @@ +import axios from "axios"; +import { Record } from "@bonfida/spl-name-service"; + +export const sendRoaRequest = async (domain: string, record: Record) => { + try { + await axios.post("https://roa-guardian.bonfida.workers.dev/roa", { + domain, + record, + }); + } catch {} +}; diff --git a/src/screens/DomainView.tsx b/src/screens/DomainView.tsx index 88302ac..1939a62 100644 --- a/src/screens/DomainView.tsx +++ b/src/screens/DomainView.tsx @@ -29,10 +29,24 @@ import { deleteInstruction, serializeRecord, serializeSolRecord, + serializeRecordV2Content, + updateRecordV2Instruction, + createRecordV2Instruction, + getRecordKeySync, + getRecordV2Key, + deleteRecordV2, + Record, + validateRecordV2Content, + GUARDIANS, + writRoaRecordV2, } from "@bonfida/spl-name-service"; import { isMobile } from "@src/utils/platform"; import { ROOT_DOMAIN } from "@bonfida/name-offers"; -import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { + PublicKey, + Transaction, + TransactionInstruction, +} from "@solana/web3.js"; import Clipboard from "@react-native-clipboard/clipboard"; import { useModal } from "react-native-modalfy"; import { useProfilePic } from "@bonfida/sns-react"; @@ -47,6 +61,7 @@ import { SOCIAL_RECORDS, useAddressRecords, useSocialRecords, + usePicRecord, } from "@src/hooks/useRecords"; import { useSolanaConnection } from "@src/hooks/xnft-hooks"; import { useDomainInfo } from "@src/hooks/useDomainInfo"; @@ -61,6 +76,8 @@ import { sendTx } from "@src/utils/send-tx"; import { sleep } from "@src/utils/sleep"; import { useHandleError } from "@src/hooks/useHandleError"; import { LoadingState } from "@src/screens/Profile/LoadingState"; +import { RecordV2Badge } from "@src/components/RecordV2Badge"; +import { sendRoaRequest } from "@src/hooks/useRecordsV2Guardians"; const getIcon = (record: SocialRecord) => { const defaultIconAttrs = { @@ -90,7 +107,7 @@ const getIcon = (record: SocialRecord) => { }; type FormKeys = AddressRecord | SocialRecord; -type FormValue = string; +type FormValue = { value: string | undefined; roa: boolean; stale: boolean }; // using Map to store correct order of fields type FormState = Map; type FormAction = @@ -116,7 +133,7 @@ export const DomainView = ({ domain }: { domain: string }) => { const addressRecords = useAddressRecords(domain); const domainInfo = useDomainInfo(domain); const subdomains = useSubdomains(domain); - const picRecord = useProfilePic(connection!, domain); + const picRecord = usePicRecord(domain); const { publicKey } = useWallet(); const isOwner = domainInfo.result?.owner === publicKey?.toBase58(); @@ -136,17 +153,15 @@ export const DomainView = ({ domain }: { domain: string }) => { const refresh = async () => { await Promise.allSettled([ domainInfo.execute(), - socialRecords.execute(), - addressRecords.execute(), + socialRecords?.execute(), + addressRecords?.execute(), picRecord.execute(), subdomains.execute(), ]); }; const scrollViewRef = useRef(null); - const [UISectionsCoordinates, setCoordinates] = useState< - Record<"socials" | "addresses" | "subdomains", number> - >({ + const [UISectionsCoordinates, setCoordinates] = useState({ socials: 0, addresses: 0, subdomains: 0, @@ -189,118 +204,59 @@ export const DomainView = ({ domain }: { domain: string }) => { for (const field of fields) { const { record, value } = field; - const sub = Buffer.from([1]).toString() + record; - let { pubkey: recordKey, isSub } = getDomainKeySync( - record + "." + domain, - true, - ); - const parent = isSub ? getDomainKeySync(domain).pubkey : ROOT_DOMAIN; - - // Check if exists - let ser: Buffer; - if (record === SNSRecord.SOL) { - const toSign = Buffer.concat([ - new PublicKey(value).toBuffer(), - recordKey.toBuffer(), - ]); - - const encodedMessage = new TextEncoder().encode(toSign.toString("hex")); - const signed = await signMessage(encodedMessage); - ser = serializeSolRecord( - new PublicKey(value), - recordKey, - publicKey, - signed, - ); - } else { - ser = serializeRecord(value, record); - } - const space = ser.length; - const currentAccount = await connection.getAccountInfo(recordKey); + const recordKey = getRecordV2Key(domain, record); + const isRoaSupported = GUARDIANS.has(record); + const currentAccount = await connection.getAccountInfo(recordKey); if (!currentAccount?.data) { - const lamports = await connection.getMinimumBalanceForRentExemption( - space + NameRegistryState.HEADER_LEN, - ); - const ix = await createNameRegistry( - connection, - sub, - space, + const ix = createRecordV2Instruction( + domain, + record, + value, publicKey, publicKey, - lamports, - undefined, - parent, ); ixs.push(ix); } else { - const { registry } = await NameRegistryState.retrieve( - connection, - recordKey, + const ix = updateRecordV2Instruction( + domain, + record, + value, + publicKey, + publicKey, ); + ixs.push(ix); + } - if (!registry.owner.equals(publicKey)) { - // Record was created before domain was transfered - const ix = transferInstruction( - NAME_PROGRAM_ID, - recordKey, - publicKey, - registry.owner, - undefined, - parent, - publicKey, - ); - ixs.push(ix); - } + ixs.push( + validateRecordV2Content( + true, + domain, + record, + publicKey, + publicKey, + publicKey, + ), + ); - // The size changed: delete + create to resize - if ( - currentAccount.data.length - NameRegistryState.HEADER_LEN !== - space - ) { - console.log("Resizing..."); - const ixClose = deleteInstruction( - NAME_PROGRAM_ID, - recordKey, - publicKey, - publicKey, - ); - const sig = await sendTx( - connection, - publicKey, - [ixClose], - signTransaction, - ); - console.log(sig); - - const lamports = await connection.getMinimumBalanceForRentExemption( - space + NameRegistryState.HEADER_LEN, - ); - const ix = await createNameRegistry( - connection, - sub, - space, + /** + * If eligible to RoA create write ix + */ + + if (isRoaSupported) { + ixs.push( + writRoaRecordV2( + domain, + record, publicKey, publicKey, - lamports, - undefined, - parent, - ); - ixs.push(ix); - } + GUARDIANS.get(record)!, + ), + ); } - const ix = updateInstruction( - NAME_PROGRAM_ID, - recordKey, - new Numberu32(0), - ser, - publicKey, - ); - - ixs.push(ix); - // Handle bridge cases + // TODO add Injective mainnet if (record === SNSRecord.BSC) { const ix = await post( ChainId.BSC, @@ -321,17 +277,8 @@ export const DomainView = ({ domain }: { domain: string }) => { if (!publicKey) return []; const ixs: TransactionInstruction[] = []; - for (const record of records) { - const { pubkey } = getDomainKeySync(record + "." + domain, true); - - const ix = deleteInstruction( - NAME_PROGRAM_ID, - pubkey, - publicKey, - publicKey, - ); - + const ix = deleteRecordV2(domain, record, publicKey, publicKey); ixs.push(ix); } @@ -347,7 +294,7 @@ export const DomainView = ({ domain }: { domain: string }) => { const fieldsToDelete: SNSRecord[] = []; for (const key of formState.keys()) { - const stateValue: string = formState.get(key) as string; + const stateValue = formState.get(key)?.value; const prevStateValue: string = previousFormState.get(key); if (stateValue !== prevStateValue) { @@ -377,7 +324,9 @@ export const DomainView = ({ domain }: { domain: string }) => { stateValue === "" ? fieldsToDelete.push(key) - : fieldsToUpdate.push({ record: key, value: stateValue }); + : stateValue + ? fieldsToUpdate.push({ record: key, value: stateValue }) + : undefined; } } @@ -400,6 +349,12 @@ export const DomainView = ({ domain }: { domain: string }) => { await sleep(400); + for (let update of fieldsToUpdate) { + if (GUARDIANS.has(update.record)) { + await sendRoaRequest(domain, update.record); + } + } + resetForm(); refresh(); } catch (err) { @@ -414,10 +369,14 @@ export const DomainView = ({ domain }: { domain: string }) => { type: "bulk", value: [...socialRecords.result, ...addressRecords.result].reduce( (acc, v) => { - acc.set(v.record, v.value || ""); + acc.set(v.record as FormKeys, { + value: v.deserialized, + roa: v.roa, + stale: v.stale, + }); return acc; }, - new Map(), + new Map(), ), }); } @@ -693,7 +652,7 @@ export const DomainView = ({ domain }: { domain: string }) => { activeOpacity={1} > { {getTranslatedName(item)} + } onChangeText={(text) => { setFormDirty(true); dispatchFormChange({ type: item, - value: text, + value: { value: text, roa: false, stale: true }, }); }} /> diff --git a/xnft.json b/xnft.json index de0e4a4..1d93e24 100644 --- a/xnft.json +++ b/xnft.json @@ -1,6 +1,6 @@ { "name": "SNS Manager (Beta)", - "version": "1.0.3", + "version": "1.1.0", "storage": "aws", "description": "All-in-one tool for SNS", "longDescription": "Your all-in-one tool for handling Solana Name Service (SNS) domains",