1- import { DebugLogger , KnownErrors } from '@layerzerolabs/io-devtools'
1+ import { DebugLogger , KnownErrors , createModuleLogger } from '@layerzerolabs/io-devtools'
2+ import { EndpointId } from '@layerzerolabs/lz-definitions'
23import { Connection , PublicKey , SystemProgram } from '@solana/web3.js'
3- import { PROGRAM_ID as SQUADS_PROGRAM_ID } from '@sqds/multisig'
4+ import { PROGRAM_ID as SQUADS_V4_PROGRAM_ID } from '@sqds/multisig'
5+
6+ // Squads V3 program ID - legacy, not ideal but we allow it
7+ const SQUADS_V3_PROGRAM_ID = new PublicKey ( 'SMPLecH534NA9acpos4G6x7uf3LWbCAwZQE9e8ZekMu' )
8+
9+ const createLogger = ( ) => createModuleLogger ( 'Solana addresses' )
410
511/**
612 * Returns true if the provided address is a valid on-curve public key. This can mean the address is either a 'regular' Solana address or a Squads Vault PDA.
713 */
814export function isOnCurveAddress ( address : string ) : boolean {
15+ const logger = createLogger ( )
916 try {
10- return PublicKey . isOnCurve ( new PublicKey ( address ) . toBytes ( ) )
17+ const result = PublicKey . isOnCurve ( new PublicKey ( address ) . toBytes ( ) )
18+ logger . debug ( `[isOnCurveAddress] address=${ address } onCurve=${ result } ` )
19+ return result
1120 } catch {
21+ logger . debug ( `[isOnCurveAddress] address=${ address } invalid public key` )
1222 return false
1323 }
1424}
1525
1626/**
17- * Note: This was created before the existence of the Squads isVault endpoint was known.
18- * Returns true if the provided address could be a Squads vault PDA.
27+ * Checks if an address is a Squads V4 vault using the Squads API.
28+ * Only works on Solana mainnet (30168). Returns null for testnet (40168).
29+ * Throws for any other EID.
1930 */
20- export async function isPossibleSquadsVault ( connection : Connection , address : string ) : Promise < boolean > {
21- try {
22- const pubkey = new PublicKey ( address )
23- const accountInfo = await connection . getAccountInfo ( pubkey )
24- const isOnCurve = isOnCurveAddress ( address )
25- if ( isOnCurve ) {
26- // a Squads Vault address is always off-curve
27- return false
28- }
31+ export async function isSquadsV4Vault ( eid : EndpointId , address : string ) : Promise < boolean | null > {
32+ const logger = createLogger ( )
2933
30- return accountInfo != null && accountInfo . owner . equals ( SystemProgram . programId )
31- } catch ( error ) {
32- return false
34+ if ( eid === EndpointId . SOLANA_V2_TESTNET ) {
35+ logger . debug ( `[isSquadsV4Vault] eid=${ eid } is testnet, returning null (API only works on mainnet)` )
36+ return null
37+ }
38+
39+ if ( eid !== EndpointId . SOLANA_V2_MAINNET ) {
40+ throw new Error ( `[isSquadsV4Vault] unsupported eid=${ eid } , only Solana mainnet (30168) is supported` )
3341 }
34- }
3542
36- export async function isSquadsV4Vault ( address : string ) : Promise < boolean > {
3743 // https://docs.squads.so/main/development/api/vault-check
3844 // Note that this endpoint is rate-limited to 25 requests per minute. It's fine if run on end-dev side but if run on a backend, it should be cached.
3945 const response = await fetch (
4046 `https://4fnetmviidiqkjzenwxe66vgoa0soerr.lambda-url.us-east-1.on.aws/isSquad/${ address } `
4147 )
4248 const data = await response . json ( )
49+ logger . debug (
50+ `[isSquadsV4Vault] address=${ address } isSquad=${ data ?. isSquad } version=${ data ?. version } status=${ response . status } `
51+ )
4352 if ( data . isSquad && data . version != 'v4' ) {
44- console . warn ( `${ address } is a Squads Vault but not v4` )
53+ logger . warn ( `${ address } is a Squads Vault but not v4` )
4554 return false
4655 } else if ( data . isSquad && data . version === 'v4' ) {
4756 return true
@@ -50,25 +59,82 @@ export async function isSquadsV4Vault(address: string): Promise<boolean> {
5059 }
5160}
5261
62+ /**
63+ * Validates that an address is acceptable as a Solana admin (owner/delegate).
64+ *
65+ * Throws if:
66+ * - Address is off-curve AND account exists AND owned by Squads V4 Program (multisig account, not vault)
67+ * - Address is off-curve AND account exists AND owned by unrecognized program
68+ *
69+ * Does NOT throw (valid) if:
70+ * - Address is on-curve (regular Solana address)
71+ * - Address is off-curve AND account does not exist (possibly unfunded Squads Vault)
72+ * - Address is off-curve AND account exists AND owned by System Program (funded Squads Vault)
73+ * - Address is off-curve AND account exists AND owned by Squads V3 Program (legacy, allowed with warning)
74+ */
5375export async function assertValidSolanaAdmin ( connection : Connection , address : string ) : Promise < void > {
76+ const logger = createLogger ( )
5477 const pubkey = new PublicKey ( address )
5578
5679 try {
80+ logger . debug ( `[assertValidSolanaAdmin] start address=${ address } ` )
5781 const accountInfo = await connection . getAccountInfo ( pubkey )
82+ const isOnCurve = isOnCurveAddress ( address )
83+
84+ // On-curve (regular address) = always valid
85+ if ( isOnCurve ) {
86+ logger . debug ( `[assertValidSolanaAdmin] address=${ address } valid: on-curve (regular address)` )
87+ return
88+ }
89+
90+ // From here: off-curve
91+ const accountExists = accountInfo != null
92+
93+ // Off-curve + no account = possibly unfunded Squads Vault
94+ if ( ! accountExists ) {
95+ logger . debug (
96+ `[assertValidSolanaAdmin] address=${ address } valid: off-curve, account does not exist (possibly unfunded Squads Vault)`
97+ )
98+ return
99+ }
58100
59- if ( accountInfo != null && accountInfo . owner . equals ( SQUADS_PROGRAM_ID ) ) {
101+ // From here: off-curve + account exists
102+ const owner = accountInfo . owner
103+
104+ // Owned by Squads V4 Program = multisig account (invalid, should use vault address)
105+ if ( owner . equals ( SQUADS_V4_PROGRAM_ID ) ) {
106+ logger . debug (
107+ `[assertValidSolanaAdmin] address=${ address } invalid: off-curve, account exists, owned by Squads V4 Program (multisig account)`
108+ )
60109 DebugLogger . printErrorAndFixSuggestion ( KnownErrors . SOLANA_OWNER_OR_DELEGATE_CANNOT_BE_MULTISIG_ACCOUNT )
61110 throw new Error (
62111 `Invalid owner/delegate address ${ address } . This is a Squads multisig account. Use the vault address instead.`
63112 )
64113 }
65114
66- if ( ! isOnCurveAddress ( address ) && ! ( await isPossibleSquadsVault ( connection , address ) ) ) {
67- DebugLogger . printErrorAndFixSuggestion ( KnownErrors . SOLANA_INVALID_OWNER_OR_DELEGATE )
68- throw new Error (
69- `Invalid owner/delegate address ${ address } . Must be a valid on -curve address or a Squads Vault PDA. `
115+ // Owned by System Program = funded Squads Vault (valid)
116+ if ( owner . equals ( SystemProgram . programId ) ) {
117+ logger . debug (
118+ `[assertValidSolanaAdmin] address= ${ address } valid: off -curve, account exists, owned by System Program (funded Squads Vault) `
70119 )
120+ return
71121 }
122+
123+ // Owned by Squads V3 Program = legacy, not ideal but allowed
124+ if ( owner . equals ( SQUADS_V3_PROGRAM_ID ) ) {
125+ logger . warn (
126+ `[assertValidSolanaAdmin] address=${ address } valid: off-curve, account exists, owned by Squads V3 Program (legacy multisig - consider migrating to V4)`
127+ )
128+ return
129+ }
130+
131+ // Owned by unrecognized program = invalid
132+ logger . debug (
133+ `[assertValidSolanaAdmin] address=${ address } invalid: off-curve, account exists, owned by unrecognized program ${ owner . toBase58 ( ) } `
134+ )
135+ throw new Error (
136+ `Invalid owner/delegate address ${ address } . Account is owned by unrecognized program ${ owner . toBase58 ( ) } .`
137+ )
72138 } catch ( error ) {
73139 if ( error instanceof Error ) {
74140 throw error
0 commit comments