// Every app supplies its own sign-in origin and eligibility policy. import { randomBytes, createPublicKey, verify } from 'node:crypto'; import { PublicKey, SYSVAR_CLOCK_PUBKEY } from '@solana/web3.js'; import { getMetadataAccountDataSerializer, Key, TokenStandard } from '@metaplex-foundation/mpl-token-metadata'; import { ALL_APPS, siteOrigin } from '../lib/partners.mjs'; import { AUDIENCE, GRANT_SIZE, audienceBytes, checkDeployment, grantPda, grantStatus, isAddress, readClock, readGrant, readState, vaultPda, } from '../lib/registry.mjs'; const tokenProgram = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); const ed25519PublicKeyPrefix = Buffer.from('302a300506032b6570032100','hex'); // Implement save, get, and atomic consume in your own session-bound storage. // A storage adapter must bind the challenge to the requesting browser's session. export async function issueChallenge({delegate,origin=AUDIENCE,signInOrigin=origin,sessionId,store}){ if(!sessionId || !isAddress(delegate) || !PublicKey.isOnCurve(new PublicKey(delegate).toBytes())) throw new Error('Invalid session or delegate.'); if (siteOrigin(origin) !== origin) throw Error('Use the configured HTTPS sign-in origin.'); if (signInOrigin !== origin && !/^http:\/\/127\.0\.0\.1:[1-9]\d{0,4}$/.test(signInOrigin)) { throw new Error('The sign-in website must match the partner.'); } const id=randomBytes(24).toString('hex'); const issued=new Date(),expires=new Date(issued.getTime()+3*60_000); const message=[ new URL(signInOrigin).host+' wants you to sign in with your Solana account:',delegate,'', 'Verify holder access for '+origin+' on Solana devnet.','','URI: '+signInOrigin,'Version: 1','Chain ID: solana:devnet', 'Nonce: '+id,'Issued At: '+issued.toISOString(),'Expiration Time: '+expires.toISOString() ].join('\n'); await store.save(id,{sessionId,origin,signInOrigin,delegate,message,expiresAt:expires.getTime()}); return {id,message,expiresAt:expires.toISOString()}; } export async function authenticateChallenge({id,signature,origin=AUDIENCE,signInOrigin=origin,sessionId,store}){ const record=await store.get(id); if(!record || !sessionId || record.origin!==origin || record.signInOrigin!==signInOrigin || record.sessionId!==sessionId || record.expiresAt<=Date.now()){ throw new Error('Sign-in challenge expired or unavailable.'); } const key=createPublicKey({key:Buffer.concat([ed25519PublicKeyPrefix,new PublicKey(record.delegate).toBuffer()]),format:'der',type:'spki'}); const sig=Buffer.from(signature,'base64'); if(sig.length!==64 || !verify(null,Buffer.from(record.message,'utf8'),key,sig)) throw new Error('Invalid wallet signature.'); if(!(await store.consume(id,sessionId,record.expiresAt))) throw new Error('Challenge was already consumed.'); return record.delegate; } function holdsToken(account,vault,mint){ if(!account || account.executable || !account.owner.equals(tokenProgram)) return false; const d=Buffer.from(account.data); return d.length===165 && d.subarray(0,32).equals(mint.toBuffer()) && d.subarray(32,64).equals(vault.toBuffer()) && d.readBigUInt64LE(64)===1n && (d[108]===1 || d[108]===2); } function isSingleMint(account){ if(!account || account.executable || !account.owner.equals(tokenProgram)) return false; const d=Buffer.from(account.data); return d.length===82 && d.readBigUInt64LE(36)===1n && d[44]===0 && d[45]===1; } const metadataProgram = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s'); const nftStandards = new Set([TokenStandard.NonFungible, TokenStandard.NonFungibleEdition, TokenStandard.ProgrammableNonFungible, TokenStandard.ProgrammableNonFungibleEdition]); function verifiedMember(info, mint, collection) { if (!info || info.executable || !info.owner.equals(metadataProgram) || info.data[0] !== Key.MetadataV1) return false; try { const [data] = getMetadataAccountDataSerializer().deserialize(info.data); return data.mint === mint.toBase58() && data.collection.__option === 'Some' && data.collection.value.verified && data.collection.value.key === collection && data.tokenStandard.__option === 'Some' && nftStandards.has(data.tokenStandard.value); } catch { return false; } } // The signed-in identity belongs to the delegate. The verified vault is returned separately. async function acceptedAudiences(origin,scope='wallet') { if (siteOrigin(origin) !== origin) throw Error('Use the configured HTTPS sign-in origin.'); return Promise.all([ALL_APPS,origin].map(value => audienceBytes(value,scope))); } async function readDelegation(client, {authenticatedDelegate,vault,asset,origin=AUDIENCE}, extraKeys=[], minContextSlot, collection) { const {connection,program}=client; const candidates=[]; for (const audience of await acceptedAudiences(origin)) { if (asset) candidates.push(grantPda(program,vault,authenticatedDelegate,audience,'asset',asset)); candidates.push(grantPda(program,vault,authenticatedDelegate,audience,'wallet',null)); } const standardCount=candidates.length; if (collection) for(const audience of await acceptedAudiences(origin,'collection')) { candidates.push(grantPda(program,vault,authenticatedDelegate,audience,'collection',collection)); } const keys=[vaultPda(program,vault),SYSVAR_CLOCK_PUBKEY,...candidates,...extraKeys]; const snapshot=await connection.getMultipleAccountsInfoAndContext(keys,{ commitment:'finalized', ...(minContextSlot === undefined ? {} : {minContextSlot}), }); const [state,clock]=snapshot.value; const time=readClock(clock); if (Math.abs(Date.now()/1000-time)>90) throw Error('Devnet state is too stale to verify.'); const slot=snapshot.context.slot; let result={eligible:false,slot,reason:'No active delegation matches this wallet and scope.'}; if (state) { const epoch=readState(program,vault,state); for (const [i,id] of candidates.entries()) { const account=snapshot.value[2+i]; if (!account) continue; const grant=readGrant(program,id,account); if (grantStatus(grant,epoch,time)==='active') { result={eligible:true,slot,vault,delegate:authenticatedDelegate,scope:i>=standardCount?'collection':grant.scope,asset:grant.asset,grant:id.toBase58(),expiresAt:grant.expiresAt}; break; } } } return {result,extra:snapshot.value.slice(2+candidates.length)}; } // A registry relationship only. Apps that promise ownership or a balance must also verify it. export async function checkDelegation(client, request) { await checkDeployment(client); return (await readDelegation(client,request)).result; } export function validateRule(rule, address, minimum) { if (rule==='wallet') return; if (!['mint','collection','balance'].includes(rule) || !isAddress(address)) throw Error('Choose a valid Solana mint or collection.'); if (rule==='balance' && (typeof minimum !== 'string' || !/^[1-9]\d{0,19}$/.test(minimum) || BigInt(minimum)>0xffffffffffffffffn)) { throw Error('Enter a positive minimum in whole base units within the SPL token range.'); } } export async function checkHolderEligibility(client, request){ await checkDeployment(client); return checkCandidate(client, request); } export async function findHolderEligibility(client, {authenticatedDelegate,origin=AUDIENCE,rule='wallet',address,minimum}) { validateRule(rule,address,minimum); await checkDeployment(client); const audiences=await acceptedAudiences(origin); if (rule==='mint' || rule==='collection') audiences.push(...await acceptedAudiences(origin,'collection')); const groups=await Promise.all(audiences.map(hash => client.connection.getProgramAccounts(client.program,{ commitment:'finalized',filters:[{dataSize:GRANT_SIZE},{memcmp:{offset:33,bytes:authenticatedDelegate}}, {memcmp:{offset:65,bytes:new PublicKey(hash).toBase58()}}], }))); const candidates=new Map(), broad=new Set(); const add=(vault,mint)=>candidates.set(vault+':'+(mint||''),{vault,qualifyingMint:mint}); for (const [index,rows] of groups.entries()) { for (const row of rows.slice(0,40)) { const grant=readGrant(client.program,row.pubkey,row.account); if(grant.delegate!==authenticatedDelegate || !grant.audienceHash.equals(audiences[index]) || grant.expiry<=Date.now()/1000) continue; if(index>=2) { if(grant.scope!=='asset') continue; if(rule==='mint') add(grant.owner,address); else if(grant.asset===address) broad.add(grant.owner); } else if(rule==='wallet') { if(grant.scope==='wallet') add(grant.owner,undefined); } else if(rule==='collection') { if(grant.scope==='asset') add(grant.owner,grant.asset); else broad.add(grant.owner); } else if(grant.scope==='wallet' || grant.asset===address) add(grant.owner,address); } } for (const vault of [...broad].slice(0,2)) { if(candidates.size>=6) break; const owner=new PublicKey(vault); const tokens=await client.connection.getTokenAccountsByOwner(owner,{programId:tokenProgram},{commitment:'finalized'}); for(const {account} of tokens.value) { if(account.data.length!==165) continue; const mint=new PublicKey(account.data.subarray(0,32)); if(holdsToken(account,owner,mint)) add(vault,mint.toBase58()); if(candidates.size>=6) break; } } for(const candidate of [...candidates.values()].slice(0,6)) { const result=await checkCandidate(client,{...candidate,authenticatedDelegate,origin,rule,minimum,collection:rule==='collection'?address:undefined}); if(result.eligible) return result; } return {eligible:false,reason:'No eligible delegation found automatically. Check a specific vault below or create a delegation.'}; } function tokenBalance(info,vault,mint) { if (!info || info.executable || !info.owner.equals(tokenProgram)) return 0n; const d=Buffer.from(info.data); if (d.length!==165 || !d.subarray(0,32).equals(mint.toBuffer()) || !d.subarray(32,64).equals(vault.toBuffer()) || ![1,2].includes(d[108])) return 0n; return d.readBigUInt64LE(64); } async function checkCandidate(client, request){ const {authenticatedDelegate,vault,qualifyingMint,collection,minimum,origin=AUDIENCE}=request; const rule=request.rule ?? (collection===undefined?'mint':'collection'); validateRule(rule,rule==='collection'?collection:qualifyingMint,minimum); const identity={authenticatedDelegate,vault,origin}; if (rule==='wallet') return (await readDelegation(client,identity)).result; const owner=new PublicKey(vault),mint=new PublicKey(qualifyingMint); const accounts=await client.connection.getTokenAccountsByOwner(owner,{mint},{commitment:'finalized'}); // Final authority comes from the common snapshot below. Deduplicate before summing balances. const balances=new Map(); for(const {account,pubkey} of accounts.value) { const amount=tokenBalance(account,owner,mint); if(rule==='balance'?amount>0n:amount===1n) balances.set(pubkey.toBase58(),amount); } const tokens=[...balances].sort((a,b)=>a[1]===b[1]?0:a[1]>b[1]?-1:1).slice(0,rule==='balance'?90:1).map(([key])=>new PublicKey(key)); if (!tokens.length) return {eligible:false,reason:'The vault has no qualifying token balance.'}; const extraKeys=[mint,...tokens]; let expectedCollection=rule==='collection'?collection:undefined; let slot=accounts.context.slot; if(rule==='mint' || rule==='collection') { const metadataKey=PublicKey.findProgramAddressSync([Buffer.from('metadata'),metadataProgram.toBuffer(),mint.toBuffer()],metadataProgram)[0]; if(rule==='mint') { // Discover a possible collection. Membership is authorized only in the final snapshot. const hint=await client.connection.getAccountInfoAndContext(metadataKey,{commitment:'finalized',minContextSlot:slot}); slot=hint.context.slot; if(hint.value?.owner.equals(metadataProgram)) { try { const [record]=getMetadataAccountDataSerializer().deserialize(hint.value.data); if(record.collection.__option==='Some' && record.collection.value.verified) expectedCollection=record.collection.value.key; } catch {} } } if(expectedCollection) extraKeys.push(metadataKey); } const {result,extra}=await readDelegation(client,{...identity,asset:qualifyingMint},extraKeys,slot,expectedCollection); if (!result.eligible) return result; const [mintInfo,...others]=extra; if(rule==='balance') { const validMint=mintInfo && !mintInfo.executable && mintInfo.owner.equals(tokenProgram) && mintInfo.data.length===82 && mintInfo.data[45]===1; const total=others.reduce((sum,info)=>sum+tokenBalance(info,owner,mint),0n); if (!validMint || total