@viwoapp/sdk 0.1.8 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -204,9 +204,22 @@ var GOVERNANCE_CONSTANTS = {
204
204
  vetoWindow: 24 * 3600,
205
205
  // 1 day
206
206
  quorumBps: 400,
207
- // 4%
208
- zkVotingEnabled: false
209
- // C-01: Disabled until proper ZK infrastructure
207
+ // 4% (C-03: abstains no longer count toward quorum)
208
+ /** Authority transfer timelock (H-02) */
209
+ authorityTransferTimelock: 24 * 3600,
210
+ // 24 hours
211
+ /** ZK private voting constants */
212
+ zk: {
213
+ /** Vote validity proof size (3 OR proofs + 1 sum proof) */
214
+ voteProofSize: 352,
215
+ /** ElGamal ciphertext size (R || C) */
216
+ ciphertextSize: 64,
217
+ /** Max committee members */
218
+ maxCommitteeSize: 5,
219
+ /** Account sizes */
220
+ privateVotingConfigSize: 680,
221
+ decryptionShareSize: 242
222
+ }
210
223
  };
211
224
  var SECURITY_CONSTANTS = {
212
225
  // H-02: Two-step authority transfer
@@ -241,8 +254,21 @@ var SECURITY_CONSTANTS = {
241
254
  // H-NEW-02: Max proof levels (supports 4B+ users)
242
255
  maxEpochBitmap: 1023,
243
256
  // H-NEW-04: Max epoch with bitmap storage (85+ years)
244
- votingPowerVerifiedOnChain: true
257
+ votingPowerVerifiedOnChain: true,
245
258
  // C-NEW-01: Params read from chain, not passed
259
+ // Audit remediation additions
260
+ /** H-AUDIT-12: Maximum concurrent sessions per user */
261
+ maxSessionsPerUser: 5,
262
+ /** C-AUDIT-15: Maximum day delta for daily budget reset (clock skew protection) */
263
+ maxDayDelta: 2,
264
+ /** H-AUDIT-09: Maximum daily activity score for transfer hook */
265
+ maxDailyActivityScore: 5e3,
266
+ /** M-AUDIT-10: Wash flag decay period (7 days) */
267
+ washFlagDecayPeriod: 7 * 24 * 3600,
268
+ /** M-18: Maximum vouch age before expiry (1 year) */
269
+ maxVouchAge: 365 * 24 * 3600,
270
+ /** C-AUDIT-11: Valid content energy tiers (1-4) */
271
+ maxContentTier: 4
246
272
  };
247
273
  var VALID_URI_PREFIXES = ["ipfs://", "https://", "ar://"];
248
274
  var MAX_URI_LENGTH = 128;
@@ -340,6 +366,22 @@ var PDAs = class {
340
366
  );
341
367
  return pda;
342
368
  }
369
+ /** H-NEW-03: Delegation PDA (one per delegator) */
370
+ getDelegation(delegator) {
371
+ const [pda] = PublicKey2.findProgramAddressSync(
372
+ [Buffer.from(SEEDS.delegation), delegator.toBuffer()],
373
+ this.programIds.governanceProtocol
374
+ );
375
+ return pda;
376
+ }
377
+ /** H-NEW-03: Delegate stats PDA (one per delegate) */
378
+ getDelegateStats(delegate) {
379
+ const [pda] = PublicKey2.findProgramAddressSync(
380
+ [Buffer.from("delegate-stats"), delegate.toBuffer()],
381
+ this.programIds.governanceProtocol
382
+ );
383
+ return pda;
384
+ }
343
385
  // SSCRE PDAs
344
386
  getRewardsPoolConfig() {
345
387
  const [pda] = PublicKey2.findProgramAddressSync(
@@ -679,6 +721,10 @@ var StakingClient = class {
679
721
  }
680
722
  /**
681
723
  * Get tier name
724
+ *
725
+ * M-05: The on-chain update_tier instruction will reject no-op tier updates
726
+ * with TierUnchanged error. Only call updateTier when the user's stake amount
727
+ * actually qualifies for a different tier.
682
728
  */
683
729
  getTierName(tier) {
684
730
  const names = ["None", "Bronze", "Silver", "Gold", "Platinum"];
@@ -790,11 +836,14 @@ var GovernanceClient = class {
790
836
  if (!accountInfo) {
791
837
  return null;
792
838
  }
839
+ const data = accountInfo.data;
793
840
  return {
794
- authority: new PublicKey4(accountInfo.data.slice(8, 40)),
795
- proposalCount: new BN3(accountInfo.data.slice(40, 48), "le"),
796
- vevcoinMint: new PublicKey4(accountInfo.data.slice(48, 80)),
797
- paused: accountInfo.data[80] !== 0
841
+ authority: new PublicKey4(data.slice(8, 40)),
842
+ proposalCount: new BN3(data.slice(40, 48), "le"),
843
+ vevcoinMint: new PublicKey4(data.slice(48, 80)),
844
+ paused: data[80] !== 0,
845
+ pendingAuthority: new PublicKey4(data.slice(81, 113)),
846
+ pendingAuthorityActivatedAt: new BN3(data.slice(113, 121), "le")
798
847
  };
799
848
  } catch {
800
849
  return null;
@@ -867,7 +916,11 @@ var GovernanceClient = class {
867
916
  proposal: new PublicKey4(data.slice(40, 72)),
868
917
  votePower: new BN3(data.slice(72, 80), "le"),
869
918
  support: data[80] !== 0,
870
- votedAt: new BN3(data.slice(81, 89), "le")
919
+ votedAt: new BN3(data.slice(81, 89), "le"),
920
+ // Private vote ciphertexts (if present, at byte offsets after public vote fields)
921
+ ctFor: new Uint8Array(data.slice(89, 153)),
922
+ ctAgainst: new Uint8Array(data.slice(153, 217)),
923
+ ctAbstain: new Uint8Array(data.slice(217, 281))
871
924
  };
872
925
  } catch {
873
926
  return null;
@@ -882,6 +935,9 @@ var GovernanceClient = class {
882
935
  }
883
936
  /**
884
937
  * Calculate user's voting power
938
+ *
939
+ * @note M-01 fix: 5A boost formula is now `1000 + ((five_a_score * 100) / 1000)`
940
+ * to fix precision loss for small scores.
885
941
  */
886
942
  async getVotingPower(user) {
887
943
  const target = user || this.client.publicKey;
@@ -924,6 +980,9 @@ var GovernanceClient = class {
924
980
  }
925
981
  /**
926
982
  * Get proposal progress
983
+ *
984
+ * @note C-03: Quorum is now calculated as votesFor + votesAgainst only.
985
+ * Abstains do NOT count toward quorum.
927
986
  */
928
987
  async getProposalProgress(proposalId) {
929
988
  const proposal = await this.getProposal(proposalId);
@@ -947,7 +1006,7 @@ var GovernanceClient = class {
947
1006
  timeRemaining
948
1007
  };
949
1008
  }
950
- // ============ Transaction Building ============
1009
+ // ============ Transaction Building (Public Voting) ============
951
1010
  /**
952
1011
  * Build create proposal transaction
953
1012
  */
@@ -963,12 +1022,12 @@ var GovernanceClient = class {
963
1022
  return tx;
964
1023
  }
965
1024
  /**
966
- * Build vote transaction
967
- *
968
- * @note v2.8.0 (C-NEW-01): Voting power parameters (vevcoin_balance, five_a_score, tier)
1025
+ * Build vote transaction (public)
1026
+ *
1027
+ * @note v2.8.0 (C-NEW-01): Voting power parameters (vevcoin_balance, five_a_score, tier)
969
1028
  * are now read from on-chain state, not passed as parameters. This prevents vote manipulation.
970
1029
  * The transaction only needs: proposal_id and choice (VoteChoice enum)
971
- *
1030
+ *
972
1031
  * @param proposalId - The proposal to vote on
973
1032
  * @param support - true = For, false = Against (use VoteChoice for more options)
974
1033
  */
@@ -998,6 +1057,217 @@ var GovernanceClient = class {
998
1057
  const tx = new Transaction3();
999
1058
  return tx;
1000
1059
  }
1060
+ // ============ Vote Delegation (H-NEW-03) ============
1061
+ /**
1062
+ * Get delegation for a user
1063
+ */
1064
+ async getDelegation(delegator) {
1065
+ const target = delegator || this.client.publicKey;
1066
+ if (!target) {
1067
+ throw new Error("No user specified and wallet not connected");
1068
+ }
1069
+ try {
1070
+ const delegationPda = this.client.pdas.getDelegation(target);
1071
+ const accountInfo = await this.client.connection.connection.getAccountInfo(delegationPda);
1072
+ if (!accountInfo) {
1073
+ return null;
1074
+ }
1075
+ const data = accountInfo.data;
1076
+ return {
1077
+ delegator: new PublicKey4(data.slice(8, 40)),
1078
+ delegate: new PublicKey4(data.slice(40, 72)),
1079
+ delegationType: data[72],
1080
+ categories: data[73],
1081
+ delegatedAmount: new BN3(data.slice(74, 82), "le"),
1082
+ delegatedAt: new BN3(data.slice(82, 90), "le"),
1083
+ expiresAt: new BN3(data.slice(90, 98), "le"),
1084
+ revocable: data[98] !== 0
1085
+ };
1086
+ } catch {
1087
+ return null;
1088
+ }
1089
+ }
1090
+ /**
1091
+ * Build delegate votes transaction
1092
+ *
1093
+ * H-NEW-03: Validates delegation amount against actual veVCoin balance on-chain.
1094
+ * The delegated amount must be <= the delegator's veVCoin balance from the staking protocol.
1095
+ *
1096
+ * @param params - Delegation parameters
1097
+ */
1098
+ async buildDelegateVotesTransaction(params) {
1099
+ if (!this.client.publicKey) {
1100
+ throw new Error("Wallet not connected");
1101
+ }
1102
+ const delegationPda = this.client.pdas.getDelegation(this.client.publicKey);
1103
+ const delegateStatsPda = this.client.pdas.getDelegateStats(params.delegate);
1104
+ const configPda = this.client.pdas.getGovernanceConfig();
1105
+ const userStakePda = this.client.pdas.getUserStake(this.client.publicKey);
1106
+ const existing = await this.getDelegation();
1107
+ if (existing) {
1108
+ throw new Error("Active delegation already exists. Revoke it first.");
1109
+ }
1110
+ const tx = new Transaction3();
1111
+ return tx;
1112
+ }
1113
+ /**
1114
+ * Build revoke delegation transaction
1115
+ */
1116
+ async buildRevokeDelegationTransaction() {
1117
+ if (!this.client.publicKey) {
1118
+ throw new Error("Wallet not connected");
1119
+ }
1120
+ const existing = await this.getDelegation();
1121
+ if (!existing) {
1122
+ throw new Error("No active delegation to revoke");
1123
+ }
1124
+ const delegationPda = this.client.pdas.getDelegation(this.client.publicKey);
1125
+ const delegateStatsPda = this.client.pdas.getDelegateStats(existing.delegate);
1126
+ const tx = new Transaction3();
1127
+ return tx;
1128
+ }
1129
+ // ============ ZK Private Voting ============
1130
+ /**
1131
+ * Build cast private vote transaction
1132
+ *
1133
+ * Uses Twisted ElGamal encryption on Ristretto255 with compressed sigma proofs.
1134
+ * The voter encrypts their choice into 3 ciphertexts (for/against/abstain) and
1135
+ * generates a validity proof that exactly one ciphertext encrypts their weight.
1136
+ *
1137
+ * Use the `zk-voting-sdk` crate to generate ciphertexts and proofs off-chain:
1138
+ * ```rust
1139
+ * let (ct_for, ct_against, ct_abstain, proof) = encrypt_and_prove(&pubkey, choice, weight);
1140
+ * ```
1141
+ *
1142
+ * @param params - Private vote parameters with ciphertexts and proof
1143
+ */
1144
+ async buildCastPrivateVoteTransaction(params) {
1145
+ if (!this.client.publicKey) {
1146
+ throw new Error("Wallet not connected");
1147
+ }
1148
+ if (params.ctFor.length !== GOVERNANCE_CONSTANTS.zk.ciphertextSize) {
1149
+ throw new Error(`ctFor must be ${GOVERNANCE_CONSTANTS.zk.ciphertextSize} bytes`);
1150
+ }
1151
+ if (params.ctAgainst.length !== GOVERNANCE_CONSTANTS.zk.ciphertextSize) {
1152
+ throw new Error(`ctAgainst must be ${GOVERNANCE_CONSTANTS.zk.ciphertextSize} bytes`);
1153
+ }
1154
+ if (params.ctAbstain.length !== GOVERNANCE_CONSTANTS.zk.ciphertextSize) {
1155
+ throw new Error(`ctAbstain must be ${GOVERNANCE_CONSTANTS.zk.ciphertextSize} bytes`);
1156
+ }
1157
+ if (params.proofData.length !== GOVERNANCE_CONSTANTS.zk.voteProofSize) {
1158
+ throw new Error(`proofData must be ${GOVERNANCE_CONSTANTS.zk.voteProofSize} bytes`);
1159
+ }
1160
+ const hasVoted = await this.hasVoted(params.proposalId);
1161
+ if (hasVoted) {
1162
+ throw new Error("Already voted on this proposal");
1163
+ }
1164
+ const tx = new Transaction3();
1165
+ return tx;
1166
+ }
1167
+ /**
1168
+ * Build enable private voting transaction
1169
+ *
1170
+ * Called by the governance authority to enable ZK private voting on a proposal.
1171
+ * Requires specifying the decryption committee and their ElGamal public keys.
1172
+ *
1173
+ * @param params - Private voting configuration
1174
+ */
1175
+ async buildEnablePrivateVotingTransaction(params) {
1176
+ if (!this.client.publicKey) {
1177
+ throw new Error("Wallet not connected");
1178
+ }
1179
+ if (params.encryptionPubkey.length !== 32) {
1180
+ throw new Error("encryptionPubkey must be 32 bytes (Ristretto255 point)");
1181
+ }
1182
+ if (params.committeeSize > GOVERNANCE_CONSTANTS.zk.maxCommitteeSize) {
1183
+ throw new Error(`Committee size exceeds max of ${GOVERNANCE_CONSTANTS.zk.maxCommitteeSize}`);
1184
+ }
1185
+ if (params.decryptionThreshold > params.committeeSize) {
1186
+ throw new Error("Threshold cannot exceed committee size");
1187
+ }
1188
+ for (let i = 0; i < params.committeeSize; i++) {
1189
+ if (!params.committeeElgamalPubkeys[i] || params.committeeElgamalPubkeys[i].length !== 32) {
1190
+ throw new Error(`Committee member ${i} ElGamal pubkey must be 32 bytes`);
1191
+ }
1192
+ }
1193
+ const tx = new Transaction3();
1194
+ return tx;
1195
+ }
1196
+ /**
1197
+ * Build submit decryption share transaction
1198
+ *
1199
+ * Called by a committee member during the reveal phase.
1200
+ * Each member computes partial decryptions and a DLEQ proof off-chain:
1201
+ * ```rust
1202
+ * let partial = generate_partial_decryption(&secret_share, &pk, &r_for, &r_against, &r_abstain);
1203
+ * ```
1204
+ *
1205
+ * @param params - Decryption share with DLEQ proof
1206
+ */
1207
+ async buildSubmitDecryptionShareTransaction(params) {
1208
+ if (!this.client.publicKey) {
1209
+ throw new Error("Wallet not connected");
1210
+ }
1211
+ const fields = [
1212
+ { name: "partialFor", value: params.partialFor },
1213
+ { name: "partialAgainst", value: params.partialAgainst },
1214
+ { name: "partialAbstain", value: params.partialAbstain },
1215
+ { name: "dleqChallenge", value: params.dleqChallenge },
1216
+ { name: "dleqResponse", value: params.dleqResponse }
1217
+ ];
1218
+ for (const { name, value } of fields) {
1219
+ if (value.length !== 32) {
1220
+ throw new Error(`${name} must be 32 bytes`);
1221
+ }
1222
+ }
1223
+ const tx = new Transaction3();
1224
+ return tx;
1225
+ }
1226
+ /**
1227
+ * Build aggregate revealed votes transaction (permissionless)
1228
+ *
1229
+ * Anyone can submit the aggregated tally since the on-chain program
1230
+ * cryptographically verifies: tally * H == C_sum - D for each category.
1231
+ * This prevents fabrication of results.
1232
+ *
1233
+ * Use the `zk-voting-sdk` to compute the tally off-chain:
1234
+ * ```rust
1235
+ * let lagrange = compute_lagrange_coefficients(&selected_indices);
1236
+ * let d = combine_partials(&lagrange, &partials);
1237
+ * let tally = recover_tally(&d, &accumulated_c, max_votes).unwrap();
1238
+ * ```
1239
+ *
1240
+ * @param params - Tally values and Lagrange coefficients
1241
+ */
1242
+ async buildAggregateRevealedVotesTransaction(params) {
1243
+ for (let i = 0; i < params.lagrangeCoefficients.length; i++) {
1244
+ if (params.lagrangeCoefficients[i].length !== 32) {
1245
+ throw new Error(`Lagrange coefficient ${i} must be 32 bytes`);
1246
+ }
1247
+ }
1248
+ const tx = new Transaction3();
1249
+ return tx;
1250
+ }
1251
+ /**
1252
+ * Check if authority transfer timelock has elapsed (H-02)
1253
+ */
1254
+ async canAcceptAuthority() {
1255
+ const config = await this.getConfig();
1256
+ if (!config) {
1257
+ return { canAccept: false, reason: "Config not found" };
1258
+ }
1259
+ if (!config.pendingAuthorityActivatedAt || config.pendingAuthorityActivatedAt.isZero()) {
1260
+ return { canAccept: false, reason: "No pending authority transfer" };
1261
+ }
1262
+ const now = Math.floor(Date.now() / 1e3);
1263
+ const timelockEnd = config.pendingAuthorityActivatedAt.toNumber() + GOVERNANCE_CONSTANTS.authorityTransferTimelock;
1264
+ if (now < timelockEnd) {
1265
+ const remaining = timelockEnd - now;
1266
+ const hours = Math.ceil(remaining / 3600);
1267
+ return { canAccept: false, reason: `Timelock: ${hours} hours remaining` };
1268
+ }
1269
+ return { canAccept: true };
1270
+ }
1001
1271
  };
1002
1272
 
1003
1273
  // src/rewards/index.ts
@@ -1137,9 +1407,16 @@ var RewardsClient = class {
1137
1407
  }
1138
1408
  /**
1139
1409
  * Calculate gasless fee for claim
1410
+ *
1411
+ * C-05: Uses ceiling division to prevent fee rounding to zero on small amounts.
1412
+ * Formula: fee = ceil(amount * feeBps / 10000)
1140
1413
  */
1141
1414
  calculateGaslessFee(amount) {
1142
- const fee = amount.muln(SSCRE_CONSTANTS.gaslessFeeBps).divn(1e4);
1415
+ const numerator = amount.muln(SSCRE_CONSTANTS.gaslessFeeBps).addn(9999);
1416
+ let fee = numerator.divn(1e4);
1417
+ if (fee.gt(amount)) {
1418
+ fee = amount;
1419
+ }
1143
1420
  return fee;
1144
1421
  }
1145
1422
  /**
@@ -1196,6 +1473,9 @@ var RewardsClient = class {
1196
1473
  // ============ Transaction Building ============
1197
1474
  /**
1198
1475
  * Build claim rewards transaction
1476
+ *
1477
+ * H-NEW-02: Merkle proof size is limited to 32 levels (supports 4B+ users).
1478
+ * Proofs exceeding this limit will be rejected on-chain with MerkleProofTooLarge.
1199
1479
  */
1200
1480
  async buildClaimTransaction(params) {
1201
1481
  if (!this.client.publicKey) {
@@ -1204,6 +1484,9 @@ var RewardsClient = class {
1204
1484
  if (params.amount.lt(new BN4(SSCRE_CONSTANTS.minClaimAmount * 1e9))) {
1205
1485
  throw new Error(`Claim amount below minimum: ${SSCRE_CONSTANTS.minClaimAmount} VCoin`);
1206
1486
  }
1487
+ if (params.merkleProof.length > 32) {
1488
+ throw new Error("Merkle proof exceeds maximum size of 32 levels");
1489
+ }
1207
1490
  const hasClaimed = await this.hasClaimedEpoch(params.epoch);
1208
1491
  if (hasClaimed) {
1209
1492
  throw new Error("Already claimed for this epoch");
@@ -1365,9 +1648,16 @@ var ViLinkClient = class {
1365
1648
  }
1366
1649
  /**
1367
1650
  * Calculate platform fee for tip
1651
+ *
1652
+ * C-06: Uses ceiling division to prevent fee rounding to zero on small amounts.
1653
+ * Formula: fee = ceil(amount * feeBps / 10000)
1368
1654
  */
1369
1655
  calculateFee(amount) {
1370
- const fee = amount.muln(VILINK_CONSTANTS.platformFeeBps).divn(1e4);
1656
+ const numerator = amount.muln(VILINK_CONSTANTS.platformFeeBps).addn(9999);
1657
+ let fee = numerator.divn(1e4);
1658
+ if (fee.gt(amount)) {
1659
+ fee = amount;
1660
+ }
1371
1661
  return {
1372
1662
  fee,
1373
1663
  net: amount.sub(fee)
@@ -1449,6 +1739,13 @@ var ViLinkClient = class {
1449
1739
  * Build execute tip action transaction
1450
1740
  * @param creator - The action creator's public key
1451
1741
  * @param nonce - M-04: The action nonce (NOT timestamp)
1742
+ *
1743
+ * H-05: The on-chain handler validates that the executor's token account has
1744
+ * no active delegation (delegate is None or delegated_amount is 0).
1745
+ * This prevents delegated tokens from being spent without explicit approval.
1746
+ *
1747
+ * C-06: Platform fee uses ceiling division to prevent zero-fee exploitation on
1748
+ * small amounts.
1452
1749
  */
1453
1750
  async buildExecuteTipAction(creator, nonce) {
1454
1751
  if (!this.client.publicKey) {
@@ -1556,6 +1853,9 @@ var GaslessClient = class {
1556
1853
  }
1557
1854
  /**
1558
1855
  * Get user gasless statistics
1856
+ *
1857
+ * H-AUDIT-12: Now includes active_sessions field for per-user session limit tracking.
1858
+ * The protocol enforces a maximum of 5 concurrent active sessions per user.
1559
1859
  */
1560
1860
  async getUserStats(user) {
1561
1861
  const target = user || this.client.publicKey;
@@ -1575,7 +1875,8 @@ var GaslessClient = class {
1575
1875
  totalSubsidized: new BN6(data.slice(48, 56), "le"),
1576
1876
  totalVcoinFees: new BN6(data.slice(56, 64), "le"),
1577
1877
  sessionsCreated: data.readUInt32LE(72),
1578
- activeSession: new PublicKey7(data.slice(76, 108))
1878
+ activeSessions: data[76],
1879
+ activeSession: new PublicKey7(data.slice(77, 109))
1579
1880
  };
1580
1881
  } catch (error) {
1581
1882
  console.warn("[ViWoSDK] gasless.getUserStats failed:", error instanceof Error ? error.message : error);
@@ -1712,6 +2013,9 @@ var GaslessClient = class {
1712
2013
  // ============ Transaction Building ============
1713
2014
  /**
1714
2015
  * Build create session key transaction
2016
+ *
2017
+ * H-AUDIT-12: The protocol enforces a maximum of 5 concurrent active sessions per user.
2018
+ * Creating a session when the limit is reached will fail with MaxSessionsReached error.
1715
2019
  */
1716
2020
  async buildCreateSessionTransaction(params) {
1717
2021
  if (!this.client.publicKey) {
@@ -1723,6 +2027,10 @@ var GaslessClient = class {
1723
2027
  if (!params.scope || params.scope === 0) {
1724
2028
  throw new Error("At least one scope required");
1725
2029
  }
2030
+ const stats = await this.getUserStats();
2031
+ if (stats && stats.activeSessions >= 5) {
2032
+ throw new Error("Maximum active sessions reached (5). Revoke an existing session first.");
2033
+ }
1726
2034
  const duration = params.durationSeconds || GASLESS_CONSTANTS.sessionDuration;
1727
2035
  const maxActions = params.maxActions || GASLESS_CONSTANTS.maxSessionActions;
1728
2036
  const maxSpend = params.maxSpend || new BN6(GASLESS_CONSTANTS.maxSessionSpend * 1e9);
@@ -1833,6 +2141,23 @@ var IdentityClient = class {
1833
2141
  return benefits[level] || [];
1834
2142
  }
1835
2143
  // ============ Transaction Building ============
2144
+ /**
2145
+ * Build subscribe transaction
2146
+ *
2147
+ * C-AUDIT-22: Non-free subscription tiers require actual USDC payment via SPL
2148
+ * transfer_checked. The transaction must include the user's USDC token account,
2149
+ * the USDC mint, and the treasury token account.
2150
+ */
2151
+ async buildSubscribeTransaction(tier) {
2152
+ if (!this.client.publicKey) {
2153
+ throw new Error("Wallet not connected");
2154
+ }
2155
+ if (tier < 0 || tier > 4) {
2156
+ throw new Error("Invalid subscription tier (0-4)");
2157
+ }
2158
+ const tx = new Transaction7();
2159
+ return tx;
2160
+ }
1836
2161
  /**
1837
2162
  * Build create identity transaction
1838
2163
  */
@@ -1985,6 +2310,12 @@ var FiveAClient = class {
1985
2310
  }
1986
2311
  /**
1987
2312
  * Check if user can vouch for another
2313
+ *
2314
+ * C-08: Mutual vouching is prevented — if the target has already vouched for you,
2315
+ * you cannot vouch for them. This is enforced on-chain via reverse_vouch_record check.
2316
+ *
2317
+ * M-18: Vouches expire after 1 year (MAX_VOUCH_AGE = 365 days). Expired vouches
2318
+ * cannot be evaluated and must be re-created.
1988
2319
  */
1989
2320
  async canVouchFor(target) {
1990
2321
  if (!this.client.publicKey) {
@@ -2027,6 +2358,12 @@ var FiveAClient = class {
2027
2358
  // ============ Transaction Building ============
2028
2359
  /**
2029
2360
  * Build vouch transaction
2361
+ *
2362
+ * C-08: On-chain handler requires a reverse_vouch_record account to verify
2363
+ * mutual vouching is not occurring. The transaction must include this PDA.
2364
+ *
2365
+ * M-18: Vouches have a maximum age of 1 year. After that, evaluate_vouch
2366
+ * will reject with VouchExpired error.
2030
2367
  */
2031
2368
  async buildVouchTransaction(target) {
2032
2369
  if (!this.client.publicKey) {
@@ -2195,6 +2532,9 @@ var ContentClient = class {
2195
2532
  }
2196
2533
  /**
2197
2534
  * Get content stats
2535
+ *
2536
+ * C-AUDIT-10: Engagement scores can only increase — the on-chain update_engagement
2537
+ * instruction enforces monotonic increase to prevent manipulation.
2198
2538
  */
2199
2539
  async getContentStats(contentId) {
2200
2540
  const content = await this.getContent(contentId);
@@ -2214,6 +2554,9 @@ var ContentClient = class {
2214
2554
  // ============ Transaction Building ============
2215
2555
  /**
2216
2556
  * Build create content transaction
2557
+ *
2558
+ * C-AUDIT-11: Energy tiers are validated on-chain (valid range: 1-4).
2559
+ * Tier determines max energy and regen rate.
2217
2560
  */
2218
2561
  async buildCreateContentTransaction(contentHash) {
2219
2562
  if (!this.client.publicKey) {