@@ -901,11 +901,49 @@ export function buildAccountInterfaceFromSources(
901901 const hasDelegate = sources . some ( src => src . parsed . delegate !== null ) ;
902902 const anyFrozen = sources . some ( src => src . parsed . isFrozen ) ;
903903 const needsConsolidation = sources . length > 1 ;
904+ const delegateTotals = new Map <
905+ string ,
906+ { delegate : PublicKey ; total : bigint ; firstIndex : number }
907+ > ( ) ;
908+ for ( let i = 0 ; i < sources . length ; i ++ ) {
909+ const src = sources [ i ] ;
910+ const delegate = src . parsed . delegate ;
911+ if ( ! delegate ) continue ;
912+ const key = delegate . toBase58 ( ) ;
913+ const delegated = src . parsed . delegatedAmount ?? src . amount ;
914+ const spendable = src . amount < delegated ? src . amount : delegated ;
915+ const existing = delegateTotals . get ( key ) ;
916+ if ( existing ) {
917+ existing . total += spendable ;
918+ } else {
919+ delegateTotals . set ( key , {
920+ delegate,
921+ total : spendable ,
922+ firstIndex : i ,
923+ } ) ;
924+ }
925+ }
926+ let canonicalDelegate : PublicKey | null = null ;
927+ let canonicalDelegatedAmount = BigInt ( 0 ) ;
928+ let canonicalFirstIndex = Number . MAX_SAFE_INTEGER ;
929+ for ( const { delegate, total, firstIndex } of delegateTotals . values ( ) ) {
930+ if (
931+ total > canonicalDelegatedAmount ||
932+ ( total === canonicalDelegatedAmount &&
933+ firstIndex < canonicalFirstIndex )
934+ ) {
935+ canonicalDelegate = delegate ;
936+ canonicalDelegatedAmount = total ;
937+ canonicalFirstIndex = firstIndex ;
938+ }
939+ }
904940
905941 const unifiedAccount : Account = {
906942 ...primarySource . parsed ,
907943 address : canonicalAddress ,
908944 amount : totalAmount ,
945+ delegate : canonicalDelegate ,
946+ delegatedAmount : canonicalDelegatedAmount ,
909947 ...( anyFrozen ? { state : AccountState . Frozen , isFrozen : true } : { } ) ,
910948 } ;
911949
@@ -924,38 +962,30 @@ export function buildAccountInterfaceFromSources(
924962/**
925963 * Spendable amount for a given authority (owner or delegate).
926964 * - If authority equals the ATA owner: full parsed.amount.
927- * - If authority is a delegate: sum over sources where delegate === authority
928- * of min(source.amount, source.delegatedAmount).
929- *
930- * For compress-and-close accounts (CompressedOnly TLV), decompress carries
931- * delegate state to the hot ATA. For approve-style accounts (no TLV), the
932- * delegate is set in token data but NOT applied to the hot ATA on decompress.
933- * The transfer-interface validates this and errors for approve-style cold
934- * sources that require loading.
965+ * - If authority is the canonical delegate: parsed.delegatedAmount (bounded by parsed.amount).
966+ * - Otherwise: 0.
935967 * @internal
936968 */
937969export function spendableAmountForAuthority (
938970 iface : AccountInterface ,
939971 authority : PublicKey ,
940972) : bigint {
941973 const owner = iface . _owner ;
942- const sources = iface . _sources ?? [ ] ;
943974 if ( owner && authority . equals ( owner ) ) {
944975 return iface . parsed . amount ;
945976 }
946- let sum = BigInt ( 0 ) ;
947- for ( const src of sources ) {
948- if ( src . parsed . delegate && authority . equals ( src . parsed . delegate ) ) {
949- const amt = src . amount ;
950- const delegated = src . parsed . delegatedAmount ?? amt ;
951- sum += amt < delegated ? amt : delegated ;
952- }
977+ const delegate = iface . parsed . delegate ;
978+ if ( delegate && authority . equals ( delegate ) ) {
979+ const delegated = iface . parsed . delegatedAmount ?? BigInt ( 0 ) ;
980+ return delegated < iface . parsed . amount
981+ ? delegated
982+ : iface . parsed . amount ;
953983 }
954- return sum ;
984+ return BigInt ( 0 ) ;
955985}
956986
957987/**
958- * Whether the given authority can sign for this ATA (is owner or delegate of at least one source ).
988+ * Whether the given authority can sign for this ATA (owner or canonical delegate ).
959989 * @internal
960990 */
961991export function isAuthorityForInterface (
@@ -964,57 +994,57 @@ export function isAuthorityForInterface(
964994) : boolean {
965995 const owner = iface . _owner ;
966996 if ( owner && authority . equals ( owner ) ) return true ;
967- const sources = iface . _sources ?? [ ] ;
968- return sources . some (
969- src =>
970- src . parsed . delegate !== null &&
971- authority . equals ( src . parsed . delegate ) ,
972- ) ;
997+ const delegate = iface . parsed . delegate ;
998+ return delegate !== null && authority . equals ( delegate ) ;
973999}
9741000
9751001/**
9761002 * @internal
977- * Filter an AccountInterface to only sources the given authority can use (owner or delegate).
978- * Preserves _owner, _mint, _isAta. Use for load/transfer when authority is delegate.
1003+ * Canonical authority projection for owner/delegate checks.
9791004 */
9801005export function filterInterfaceForAuthority (
9811006 iface : AccountInterface ,
9821007 authority : PublicKey ,
9831008) : AccountInterface {
984- const sources = iface . _sources ?? [ ] ;
9851009 const owner = iface . _owner ;
986- const filtered = sources . filter (
987- src =>
988- ( owner && authority . equals ( owner ) ) ||
989- ( src . parsed . delegate !== null &&
990- authority . equals ( src . parsed . delegate ) ) ,
991- ) ;
992- if ( filtered . length === 0 ) {
1010+ if ( owner && authority . equals ( owner ) ) {
1011+ return iface ;
1012+ }
1013+ const spendable = spendableAmountForAuthority ( iface , authority ) ;
1014+ const canonicalDelegate = iface . parsed . delegate ;
1015+ if (
1016+ spendable === BigInt ( 0 ) ||
1017+ canonicalDelegate === null ||
1018+ ! authority . equals ( canonicalDelegate )
1019+ ) {
9931020 return {
9941021 ...iface ,
9951022 _sources : [ ] ,
1023+ _needsConsolidation : false ,
9961024 parsed : { ...iface . parsed , amount : BigInt ( 0 ) } ,
9971025 } ;
9981026 }
999- const spendable = spendableAmountForAuthority ( iface , authority ) ;
1027+ const sources = iface . _sources ?? [ ] ;
1028+ const filtered = sources . filter (
1029+ src =>
1030+ src . parsed . delegate !== null &&
1031+ src . parsed . delegate . equals ( canonicalDelegate ) ,
1032+ ) ;
10001033 const primary = filtered [ 0 ] ;
1001- const anyFrozen = filtered . some ( s => s . parsed . isFrozen ) ;
10021034 return {
10031035 ...iface ,
1036+ ...( primary
1037+ ? {
1038+ accountInfo : primary . accountInfo ! ,
1039+ isCold : isColdSourceType ( primary . type ) ,
1040+ loadContext : primary . loadContext ,
1041+ }
1042+ : { } ) ,
10041043 _sources : filtered ,
1005- accountInfo : primary . accountInfo ! ,
1044+ _needsConsolidation : filtered . length > 1 ,
10061045 parsed : {
1007- ...primary . parsed ,
1008- address : iface . parsed . address ,
1046+ ...iface . parsed ,
10091047 amount : spendable ,
1010- ...( anyFrozen
1011- ? { state : AccountState . Frozen , isFrozen : true }
1012- : { } ) ,
10131048 } ,
1014- isCold : isColdSourceType ( primary . type ) ,
1015- loadContext : primary . loadContext ,
1016- _needsConsolidation : filtered . length > 1 ,
1017- _hasDelegate : filtered . some ( s => s . parsed . delegate !== null ) ,
1018- _anyFrozen : anyFrozen ,
10191049 } ;
10201050}
0 commit comments