@viwoapp/sdk 0.1.8 → 2.0.0

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.js CHANGED
@@ -273,9 +273,22 @@ var GOVERNANCE_CONSTANTS = {
273
273
  vetoWindow: 24 * 3600,
274
274
  // 1 day
275
275
  quorumBps: 400,
276
- // 4%
277
- zkVotingEnabled: false
278
- // C-01: Disabled until proper ZK infrastructure
276
+ // 4% (C-03: abstains no longer count toward quorum)
277
+ /** Authority transfer timelock (H-02) */
278
+ authorityTransferTimelock: 24 * 3600,
279
+ // 24 hours
280
+ /** ZK private voting constants */
281
+ zk: {
282
+ /** Vote validity proof size (3 OR proofs + 1 sum proof) */
283
+ voteProofSize: 352,
284
+ /** ElGamal ciphertext size (R || C) */
285
+ ciphertextSize: 64,
286
+ /** Max committee members */
287
+ maxCommitteeSize: 5,
288
+ /** Account sizes */
289
+ privateVotingConfigSize: 680,
290
+ decryptionShareSize: 242
291
+ }
279
292
  };
280
293
  var SECURITY_CONSTANTS = {
281
294
  // H-02: Two-step authority transfer
@@ -310,8 +323,21 @@ var SECURITY_CONSTANTS = {
310
323
  // H-NEW-02: Max proof levels (supports 4B+ users)
311
324
  maxEpochBitmap: 1023,
312
325
  // H-NEW-04: Max epoch with bitmap storage (85+ years)
313
- votingPowerVerifiedOnChain: true
326
+ votingPowerVerifiedOnChain: true,
314
327
  // C-NEW-01: Params read from chain, not passed
328
+ // Audit remediation additions
329
+ /** H-AUDIT-12: Maximum concurrent sessions per user */
330
+ maxSessionsPerUser: 5,
331
+ /** C-AUDIT-15: Maximum day delta for daily budget reset (clock skew protection) */
332
+ maxDayDelta: 2,
333
+ /** H-AUDIT-09: Maximum daily activity score for transfer hook */
334
+ maxDailyActivityScore: 5e3,
335
+ /** M-AUDIT-10: Wash flag decay period (7 days) */
336
+ washFlagDecayPeriod: 7 * 24 * 3600,
337
+ /** M-18: Maximum vouch age before expiry (1 year) */
338
+ maxVouchAge: 365 * 24 * 3600,
339
+ /** C-AUDIT-11: Valid content energy tiers (1-4) */
340
+ maxContentTier: 4
315
341
  };
316
342
  var VALID_URI_PREFIXES = ["ipfs://", "https://", "ar://"];
317
343
  var MAX_URI_LENGTH = 128;
@@ -748,6 +774,10 @@ var StakingClient = class {
748
774
  }
749
775
  /**
750
776
  * Get tier name
777
+ *
778
+ * M-05: The on-chain update_tier instruction will reject no-op tier updates
779
+ * with TierUnchanged error. Only call updateTier when the user's stake amount
780
+ * actually qualifies for a different tier.
751
781
  */
752
782
  getTierName(tier) {
753
783
  const names = ["None", "Bronze", "Silver", "Gold", "Platinum"];
@@ -859,11 +889,14 @@ var GovernanceClient = class {
859
889
  if (!accountInfo) {
860
890
  return null;
861
891
  }
892
+ const data = accountInfo.data;
862
893
  return {
863
- authority: new import_web34.PublicKey(accountInfo.data.slice(8, 40)),
864
- proposalCount: new import_anchor3.BN(accountInfo.data.slice(40, 48), "le"),
865
- vevcoinMint: new import_web34.PublicKey(accountInfo.data.slice(48, 80)),
866
- paused: accountInfo.data[80] !== 0
894
+ authority: new import_web34.PublicKey(data.slice(8, 40)),
895
+ proposalCount: new import_anchor3.BN(data.slice(40, 48), "le"),
896
+ vevcoinMint: new import_web34.PublicKey(data.slice(48, 80)),
897
+ paused: data[80] !== 0,
898
+ pendingAuthority: new import_web34.PublicKey(data.slice(81, 113)),
899
+ pendingAuthorityActivatedAt: new import_anchor3.BN(data.slice(113, 121), "le")
867
900
  };
868
901
  } catch {
869
902
  return null;
@@ -936,7 +969,11 @@ var GovernanceClient = class {
936
969
  proposal: new import_web34.PublicKey(data.slice(40, 72)),
937
970
  votePower: new import_anchor3.BN(data.slice(72, 80), "le"),
938
971
  support: data[80] !== 0,
939
- votedAt: new import_anchor3.BN(data.slice(81, 89), "le")
972
+ votedAt: new import_anchor3.BN(data.slice(81, 89), "le"),
973
+ // Private vote ciphertexts (if present, at byte offsets after public vote fields)
974
+ ctFor: new Uint8Array(data.slice(89, 153)),
975
+ ctAgainst: new Uint8Array(data.slice(153, 217)),
976
+ ctAbstain: new Uint8Array(data.slice(217, 281))
940
977
  };
941
978
  } catch {
942
979
  return null;
@@ -951,6 +988,9 @@ var GovernanceClient = class {
951
988
  }
952
989
  /**
953
990
  * Calculate user's voting power
991
+ *
992
+ * @note M-01 fix: 5A boost formula is now `1000 + ((five_a_score * 100) / 1000)`
993
+ * to fix precision loss for small scores.
954
994
  */
955
995
  async getVotingPower(user) {
956
996
  const target = user || this.client.publicKey;
@@ -993,6 +1033,9 @@ var GovernanceClient = class {
993
1033
  }
994
1034
  /**
995
1035
  * Get proposal progress
1036
+ *
1037
+ * @note C-03: Quorum is now calculated as votesFor + votesAgainst only.
1038
+ * Abstains do NOT count toward quorum.
996
1039
  */
997
1040
  async getProposalProgress(proposalId) {
998
1041
  const proposal = await this.getProposal(proposalId);
@@ -1016,7 +1059,7 @@ var GovernanceClient = class {
1016
1059
  timeRemaining
1017
1060
  };
1018
1061
  }
1019
- // ============ Transaction Building ============
1062
+ // ============ Transaction Building (Public Voting) ============
1020
1063
  /**
1021
1064
  * Build create proposal transaction
1022
1065
  */
@@ -1032,12 +1075,12 @@ var GovernanceClient = class {
1032
1075
  return tx;
1033
1076
  }
1034
1077
  /**
1035
- * Build vote transaction
1036
- *
1037
- * @note v2.8.0 (C-NEW-01): Voting power parameters (vevcoin_balance, five_a_score, tier)
1078
+ * Build vote transaction (public)
1079
+ *
1080
+ * @note v2.8.0 (C-NEW-01): Voting power parameters (vevcoin_balance, five_a_score, tier)
1038
1081
  * are now read from on-chain state, not passed as parameters. This prevents vote manipulation.
1039
1082
  * The transaction only needs: proposal_id and choice (VoteChoice enum)
1040
- *
1083
+ *
1041
1084
  * @param proposalId - The proposal to vote on
1042
1085
  * @param support - true = For, false = Against (use VoteChoice for more options)
1043
1086
  */
@@ -1067,6 +1110,148 @@ var GovernanceClient = class {
1067
1110
  const tx = new import_web34.Transaction();
1068
1111
  return tx;
1069
1112
  }
1113
+ // ============ ZK Private Voting ============
1114
+ /**
1115
+ * Build cast private vote transaction
1116
+ *
1117
+ * Uses Twisted ElGamal encryption on Ristretto255 with compressed sigma proofs.
1118
+ * The voter encrypts their choice into 3 ciphertexts (for/against/abstain) and
1119
+ * generates a validity proof that exactly one ciphertext encrypts their weight.
1120
+ *
1121
+ * Use the `zk-voting-sdk` crate to generate ciphertexts and proofs off-chain:
1122
+ * ```rust
1123
+ * let (ct_for, ct_against, ct_abstain, proof) = encrypt_and_prove(&pubkey, choice, weight);
1124
+ * ```
1125
+ *
1126
+ * @param params - Private vote parameters with ciphertexts and proof
1127
+ */
1128
+ async buildCastPrivateVoteTransaction(params) {
1129
+ if (!this.client.publicKey) {
1130
+ throw new Error("Wallet not connected");
1131
+ }
1132
+ if (params.ctFor.length !== GOVERNANCE_CONSTANTS.zk.ciphertextSize) {
1133
+ throw new Error(`ctFor must be ${GOVERNANCE_CONSTANTS.zk.ciphertextSize} bytes`);
1134
+ }
1135
+ if (params.ctAgainst.length !== GOVERNANCE_CONSTANTS.zk.ciphertextSize) {
1136
+ throw new Error(`ctAgainst must be ${GOVERNANCE_CONSTANTS.zk.ciphertextSize} bytes`);
1137
+ }
1138
+ if (params.ctAbstain.length !== GOVERNANCE_CONSTANTS.zk.ciphertextSize) {
1139
+ throw new Error(`ctAbstain must be ${GOVERNANCE_CONSTANTS.zk.ciphertextSize} bytes`);
1140
+ }
1141
+ if (params.proofData.length !== GOVERNANCE_CONSTANTS.zk.voteProofSize) {
1142
+ throw new Error(`proofData must be ${GOVERNANCE_CONSTANTS.zk.voteProofSize} bytes`);
1143
+ }
1144
+ const hasVoted = await this.hasVoted(params.proposalId);
1145
+ if (hasVoted) {
1146
+ throw new Error("Already voted on this proposal");
1147
+ }
1148
+ const tx = new import_web34.Transaction();
1149
+ return tx;
1150
+ }
1151
+ /**
1152
+ * Build enable private voting transaction
1153
+ *
1154
+ * Called by the governance authority to enable ZK private voting on a proposal.
1155
+ * Requires specifying the decryption committee and their ElGamal public keys.
1156
+ *
1157
+ * @param params - Private voting configuration
1158
+ */
1159
+ async buildEnablePrivateVotingTransaction(params) {
1160
+ if (!this.client.publicKey) {
1161
+ throw new Error("Wallet not connected");
1162
+ }
1163
+ if (params.encryptionPubkey.length !== 32) {
1164
+ throw new Error("encryptionPubkey must be 32 bytes (Ristretto255 point)");
1165
+ }
1166
+ if (params.committeeSize > GOVERNANCE_CONSTANTS.zk.maxCommitteeSize) {
1167
+ throw new Error(`Committee size exceeds max of ${GOVERNANCE_CONSTANTS.zk.maxCommitteeSize}`);
1168
+ }
1169
+ if (params.decryptionThreshold > params.committeeSize) {
1170
+ throw new Error("Threshold cannot exceed committee size");
1171
+ }
1172
+ for (let i = 0; i < params.committeeSize; i++) {
1173
+ if (!params.committeeElgamalPubkeys[i] || params.committeeElgamalPubkeys[i].length !== 32) {
1174
+ throw new Error(`Committee member ${i} ElGamal pubkey must be 32 bytes`);
1175
+ }
1176
+ }
1177
+ const tx = new import_web34.Transaction();
1178
+ return tx;
1179
+ }
1180
+ /**
1181
+ * Build submit decryption share transaction
1182
+ *
1183
+ * Called by a committee member during the reveal phase.
1184
+ * Each member computes partial decryptions and a DLEQ proof off-chain:
1185
+ * ```rust
1186
+ * let partial = generate_partial_decryption(&secret_share, &pk, &r_for, &r_against, &r_abstain);
1187
+ * ```
1188
+ *
1189
+ * @param params - Decryption share with DLEQ proof
1190
+ */
1191
+ async buildSubmitDecryptionShareTransaction(params) {
1192
+ if (!this.client.publicKey) {
1193
+ throw new Error("Wallet not connected");
1194
+ }
1195
+ const fields = [
1196
+ { name: "partialFor", value: params.partialFor },
1197
+ { name: "partialAgainst", value: params.partialAgainst },
1198
+ { name: "partialAbstain", value: params.partialAbstain },
1199
+ { name: "dleqChallenge", value: params.dleqChallenge },
1200
+ { name: "dleqResponse", value: params.dleqResponse }
1201
+ ];
1202
+ for (const { name, value } of fields) {
1203
+ if (value.length !== 32) {
1204
+ throw new Error(`${name} must be 32 bytes`);
1205
+ }
1206
+ }
1207
+ const tx = new import_web34.Transaction();
1208
+ return tx;
1209
+ }
1210
+ /**
1211
+ * Build aggregate revealed votes transaction (permissionless)
1212
+ *
1213
+ * Anyone can submit the aggregated tally since the on-chain program
1214
+ * cryptographically verifies: tally * H == C_sum - D for each category.
1215
+ * This prevents fabrication of results.
1216
+ *
1217
+ * Use the `zk-voting-sdk` to compute the tally off-chain:
1218
+ * ```rust
1219
+ * let lagrange = compute_lagrange_coefficients(&selected_indices);
1220
+ * let d = combine_partials(&lagrange, &partials);
1221
+ * let tally = recover_tally(&d, &accumulated_c, max_votes).unwrap();
1222
+ * ```
1223
+ *
1224
+ * @param params - Tally values and Lagrange coefficients
1225
+ */
1226
+ async buildAggregateRevealedVotesTransaction(params) {
1227
+ for (let i = 0; i < params.lagrangeCoefficients.length; i++) {
1228
+ if (params.lagrangeCoefficients[i].length !== 32) {
1229
+ throw new Error(`Lagrange coefficient ${i} must be 32 bytes`);
1230
+ }
1231
+ }
1232
+ const tx = new import_web34.Transaction();
1233
+ return tx;
1234
+ }
1235
+ /**
1236
+ * Check if authority transfer timelock has elapsed (H-02)
1237
+ */
1238
+ async canAcceptAuthority() {
1239
+ const config = await this.getConfig();
1240
+ if (!config) {
1241
+ return { canAccept: false, reason: "Config not found" };
1242
+ }
1243
+ if (!config.pendingAuthorityActivatedAt || config.pendingAuthorityActivatedAt.isZero()) {
1244
+ return { canAccept: false, reason: "No pending authority transfer" };
1245
+ }
1246
+ const now = Math.floor(Date.now() / 1e3);
1247
+ const timelockEnd = config.pendingAuthorityActivatedAt.toNumber() + GOVERNANCE_CONSTANTS.authorityTransferTimelock;
1248
+ if (now < timelockEnd) {
1249
+ const remaining = timelockEnd - now;
1250
+ const hours = Math.ceil(remaining / 3600);
1251
+ return { canAccept: false, reason: `Timelock: ${hours} hours remaining` };
1252
+ }
1253
+ return { canAccept: true };
1254
+ }
1070
1255
  };
1071
1256
 
1072
1257
  // src/rewards/index.ts
@@ -1206,9 +1391,16 @@ var RewardsClient = class {
1206
1391
  }
1207
1392
  /**
1208
1393
  * Calculate gasless fee for claim
1394
+ *
1395
+ * C-05: Uses ceiling division to prevent fee rounding to zero on small amounts.
1396
+ * Formula: fee = ceil(amount * feeBps / 10000)
1209
1397
  */
1210
1398
  calculateGaslessFee(amount) {
1211
- const fee = amount.muln(SSCRE_CONSTANTS.gaslessFeeBps).divn(1e4);
1399
+ const numerator = amount.muln(SSCRE_CONSTANTS.gaslessFeeBps).addn(9999);
1400
+ let fee = numerator.divn(1e4);
1401
+ if (fee.gt(amount)) {
1402
+ fee = amount;
1403
+ }
1212
1404
  return fee;
1213
1405
  }
1214
1406
  /**
@@ -1265,6 +1457,9 @@ var RewardsClient = class {
1265
1457
  // ============ Transaction Building ============
1266
1458
  /**
1267
1459
  * Build claim rewards transaction
1460
+ *
1461
+ * H-NEW-02: Merkle proof size is limited to 32 levels (supports 4B+ users).
1462
+ * Proofs exceeding this limit will be rejected on-chain with MerkleProofTooLarge.
1268
1463
  */
1269
1464
  async buildClaimTransaction(params) {
1270
1465
  if (!this.client.publicKey) {
@@ -1273,6 +1468,9 @@ var RewardsClient = class {
1273
1468
  if (params.amount.lt(new import_anchor4.BN(SSCRE_CONSTANTS.minClaimAmount * 1e9))) {
1274
1469
  throw new Error(`Claim amount below minimum: ${SSCRE_CONSTANTS.minClaimAmount} VCoin`);
1275
1470
  }
1471
+ if (params.merkleProof.length > 32) {
1472
+ throw new Error("Merkle proof exceeds maximum size of 32 levels");
1473
+ }
1276
1474
  const hasClaimed = await this.hasClaimedEpoch(params.epoch);
1277
1475
  if (hasClaimed) {
1278
1476
  throw new Error("Already claimed for this epoch");
@@ -1434,9 +1632,16 @@ var ViLinkClient = class {
1434
1632
  }
1435
1633
  /**
1436
1634
  * Calculate platform fee for tip
1635
+ *
1636
+ * C-06: Uses ceiling division to prevent fee rounding to zero on small amounts.
1637
+ * Formula: fee = ceil(amount * feeBps / 10000)
1437
1638
  */
1438
1639
  calculateFee(amount) {
1439
- const fee = amount.muln(VILINK_CONSTANTS.platformFeeBps).divn(1e4);
1640
+ const numerator = amount.muln(VILINK_CONSTANTS.platformFeeBps).addn(9999);
1641
+ let fee = numerator.divn(1e4);
1642
+ if (fee.gt(amount)) {
1643
+ fee = amount;
1644
+ }
1440
1645
  return {
1441
1646
  fee,
1442
1647
  net: amount.sub(fee)
@@ -1518,6 +1723,13 @@ var ViLinkClient = class {
1518
1723
  * Build execute tip action transaction
1519
1724
  * @param creator - The action creator's public key
1520
1725
  * @param nonce - M-04: The action nonce (NOT timestamp)
1726
+ *
1727
+ * H-05: The on-chain handler validates that the executor's token account has
1728
+ * no active delegation (delegate is None or delegated_amount is 0).
1729
+ * This prevents delegated tokens from being spent without explicit approval.
1730
+ *
1731
+ * C-06: Platform fee uses ceiling division to prevent zero-fee exploitation on
1732
+ * small amounts.
1521
1733
  */
1522
1734
  async buildExecuteTipAction(creator, nonce) {
1523
1735
  if (!this.client.publicKey) {
@@ -1625,6 +1837,9 @@ var GaslessClient = class {
1625
1837
  }
1626
1838
  /**
1627
1839
  * Get user gasless statistics
1840
+ *
1841
+ * H-AUDIT-12: Now includes active_sessions field for per-user session limit tracking.
1842
+ * The protocol enforces a maximum of 5 concurrent active sessions per user.
1628
1843
  */
1629
1844
  async getUserStats(user) {
1630
1845
  const target = user || this.client.publicKey;
@@ -1644,7 +1859,8 @@ var GaslessClient = class {
1644
1859
  totalSubsidized: new import_anchor6.BN(data.slice(48, 56), "le"),
1645
1860
  totalVcoinFees: new import_anchor6.BN(data.slice(56, 64), "le"),
1646
1861
  sessionsCreated: data.readUInt32LE(72),
1647
- activeSession: new import_web37.PublicKey(data.slice(76, 108))
1862
+ activeSessions: data[76],
1863
+ activeSession: new import_web37.PublicKey(data.slice(77, 109))
1648
1864
  };
1649
1865
  } catch (error) {
1650
1866
  console.warn("[ViWoSDK] gasless.getUserStats failed:", error instanceof Error ? error.message : error);
@@ -1781,6 +1997,9 @@ var GaslessClient = class {
1781
1997
  // ============ Transaction Building ============
1782
1998
  /**
1783
1999
  * Build create session key transaction
2000
+ *
2001
+ * H-AUDIT-12: The protocol enforces a maximum of 5 concurrent active sessions per user.
2002
+ * Creating a session when the limit is reached will fail with MaxSessionsReached error.
1784
2003
  */
1785
2004
  async buildCreateSessionTransaction(params) {
1786
2005
  if (!this.client.publicKey) {
@@ -1792,6 +2011,10 @@ var GaslessClient = class {
1792
2011
  if (!params.scope || params.scope === 0) {
1793
2012
  throw new Error("At least one scope required");
1794
2013
  }
2014
+ const stats = await this.getUserStats();
2015
+ if (stats && stats.activeSessions >= 5) {
2016
+ throw new Error("Maximum active sessions reached (5). Revoke an existing session first.");
2017
+ }
1795
2018
  const duration = params.durationSeconds || GASLESS_CONSTANTS.sessionDuration;
1796
2019
  const maxActions = params.maxActions || GASLESS_CONSTANTS.maxSessionActions;
1797
2020
  const maxSpend = params.maxSpend || new import_anchor6.BN(GASLESS_CONSTANTS.maxSessionSpend * 1e9);
@@ -1902,6 +2125,23 @@ var IdentityClient = class {
1902
2125
  return benefits[level] || [];
1903
2126
  }
1904
2127
  // ============ Transaction Building ============
2128
+ /**
2129
+ * Build subscribe transaction
2130
+ *
2131
+ * C-AUDIT-22: Non-free subscription tiers require actual USDC payment via SPL
2132
+ * transfer_checked. The transaction must include the user's USDC token account,
2133
+ * the USDC mint, and the treasury token account.
2134
+ */
2135
+ async buildSubscribeTransaction(tier) {
2136
+ if (!this.client.publicKey) {
2137
+ throw new Error("Wallet not connected");
2138
+ }
2139
+ if (tier < 0 || tier > 4) {
2140
+ throw new Error("Invalid subscription tier (0-4)");
2141
+ }
2142
+ const tx = new import_web38.Transaction();
2143
+ return tx;
2144
+ }
1905
2145
  /**
1906
2146
  * Build create identity transaction
1907
2147
  */
@@ -2054,6 +2294,12 @@ var FiveAClient = class {
2054
2294
  }
2055
2295
  /**
2056
2296
  * Check if user can vouch for another
2297
+ *
2298
+ * C-08: Mutual vouching is prevented — if the target has already vouched for you,
2299
+ * you cannot vouch for them. This is enforced on-chain via reverse_vouch_record check.
2300
+ *
2301
+ * M-18: Vouches expire after 1 year (MAX_VOUCH_AGE = 365 days). Expired vouches
2302
+ * cannot be evaluated and must be re-created.
2057
2303
  */
2058
2304
  async canVouchFor(target) {
2059
2305
  if (!this.client.publicKey) {
@@ -2096,6 +2342,12 @@ var FiveAClient = class {
2096
2342
  // ============ Transaction Building ============
2097
2343
  /**
2098
2344
  * Build vouch transaction
2345
+ *
2346
+ * C-08: On-chain handler requires a reverse_vouch_record account to verify
2347
+ * mutual vouching is not occurring. The transaction must include this PDA.
2348
+ *
2349
+ * M-18: Vouches have a maximum age of 1 year. After that, evaluate_vouch
2350
+ * will reject with VouchExpired error.
2099
2351
  */
2100
2352
  async buildVouchTransaction(target) {
2101
2353
  if (!this.client.publicKey) {
@@ -2264,6 +2516,9 @@ var ContentClient = class {
2264
2516
  }
2265
2517
  /**
2266
2518
  * Get content stats
2519
+ *
2520
+ * C-AUDIT-10: Engagement scores can only increase — the on-chain update_engagement
2521
+ * instruction enforces monotonic increase to prevent manipulation.
2267
2522
  */
2268
2523
  async getContentStats(contentId) {
2269
2524
  const content = await this.getContent(contentId);
@@ -2283,6 +2538,9 @@ var ContentClient = class {
2283
2538
  // ============ Transaction Building ============
2284
2539
  /**
2285
2540
  * Build create content transaction
2541
+ *
2542
+ * C-AUDIT-11: Energy tiers are validated on-chain (valid range: 1-4).
2543
+ * Tier determines max energy and regen rate.
2286
2544
  */
2287
2545
  async buildCreateContentTransaction(contentHash) {
2288
2546
  if (!this.client.publicKey) {