• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hicommonwealth / commonwealth / 14092672152

26 Mar 2025 08:18PM UTC coverage: 45.157% (+0.7%) from 44.489%
14092672152

push

github

web-flow
Merge pull request #11621 from hicommonwealth/rotorsoft/11603-membership-in-quest

Refactors get memberships query, and connects quest to group refresh events

1468 of 3653 branches covered (40.19%)

Branch coverage included in aggregate %.

34 of 72 new or added lines in 6 files covered. (47.22%)

9 existing lines in 2 files now uncovered.

2807 of 5814 relevant lines covered (48.28%)

39.27 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

50.79
/libs/model/src/utils/validateGroupMembership.ts
1
import { MembershipRejectReason } from '@hicommonwealth/schemas';
2
import {
3
  AllowlistData,
4
  BalanceSourceType,
5
  Requirement,
6
  ThresholdData,
7
} from '@hicommonwealth/shared';
8
import { toBigInt } from 'web3-utils';
9
import { z } from 'zod';
10
import type { OptionsWithBalances } from '../services';
11

12
export type ValidateGroupMembershipResponse = {
13
  isValid: boolean;
14
  messages?: z.infer<typeof MembershipRejectReason>;
15
  numRequirementsMet?: number;
16
};
17

18
/**
19
 * Validates if a given user address passes a set of requirements and grants access to group
20
 * @param userAddress address of user
21
 * @param requirements An array of requirement types to be validated against
22
 * @param balances address balances
23
 * @param numRequiredRequirements
24
 * @returns ValidateGroupMembershipResponse validity and messages on requirements that failed
25
 */
26
export function validateGroupMembership(
27
  userAddress: string,
28
  requirements: Requirement[],
29
  balances: OptionsWithBalances[],
30
  numRequiredRequirements: number = 0,
2✔
31
): ValidateGroupMembershipResponse {
32
  const response: ValidateGroupMembershipResponse = {
9✔
33
    isValid: true,
34
    messages: [],
35
  };
36
  let allowListOverride = false;
9✔
37
  let numRequirementsMet = 0;
9✔
38

39
  requirements.forEach((requirement) => {
9✔
40
    let checkResult: { result: boolean; message: string };
41
    switch (requirement.rule) {
19!
42
      case 'threshold': {
43
        checkResult = _thresholdCheck(userAddress, requirement.data, balances);
19✔
44
        break;
19✔
45
      }
46
      case 'allow': {
47
        checkResult = _allowlistCheck(
×
48
          userAddress,
49
          requirement.data as AllowlistData,
50
        );
51
        if (checkResult.result) {
×
52
          allowListOverride = true;
×
53
        }
54
        break;
×
55
      }
56
      default:
57
        checkResult = {
×
58
          result: false,
59
          message: 'Invalid Requirement',
60
        };
61
        break;
×
62
    }
63

64
    if (checkResult.result) {
19✔
65
      numRequirementsMet++;
11✔
66
    } else {
67
      response.isValid = false;
8✔
68
      // @ts-expect-error StrictNullChecks
69
      response.messages.push({
8✔
70
        requirement,
71
        message: checkResult.message,
72
      });
73
    }
74
  });
75

76
  if (allowListOverride) {
9!
77
    // allow if address is whitelisted
NEW
78
    return { isValid: true, messages: undefined };
×
79
  }
80

81
  if (numRequiredRequirements) {
9✔
82
    if (numRequirementsMet >= numRequiredRequirements) {
7✔
83
      // allow if minimum number of requirements met
84
      return { ...response, isValid: true, numRequirementsMet };
6✔
85
    } else {
86
      return { ...response, isValid: false, numRequirementsMet };
1✔
87
    }
88
  }
89
  return response;
2✔
90
}
91

92
function _thresholdCheck(
93
  userAddress: string,
94
  thresholdData: ThresholdData,
95
  balances: OptionsWithBalances[],
96
): { result: boolean; message: string } {
97
  try {
19✔
98
    let balanceSourceType: BalanceSourceType;
99
    let contractAddress: string;
100
    let chainId: string;
101
    let tokenId: string;
102
    switch (thresholdData.source.source_type) {
19!
103
      case 'spl': {
104
        balanceSourceType = BalanceSourceType.SPL;
×
105
        contractAddress = thresholdData.source.contract_address;
×
106
        chainId = thresholdData.source.evm_chain_id.toString();
×
107
        break;
×
108
      }
109
      case 'erc20': {
110
        balanceSourceType = BalanceSourceType.ERC20;
6✔
111
        contractAddress = thresholdData.source.contract_address;
6✔
112
        chainId = thresholdData.source.evm_chain_id.toString();
6✔
113
        break;
6✔
114
      }
115
      case 'erc721': {
116
        balanceSourceType = BalanceSourceType.ERC721;
6✔
117
        contractAddress = thresholdData.source.contract_address;
6✔
118
        chainId = thresholdData.source.evm_chain_id.toString();
6✔
119
        break;
6✔
120
      }
121
      case 'erc1155': {
122
        balanceSourceType = BalanceSourceType.ERC1155;
×
123
        contractAddress = thresholdData.source.contract_address;
×
124
        chainId = thresholdData.source.evm_chain_id.toString();
×
125
        // @ts-expect-error StrictNullChecks
126
        tokenId = thresholdData.source.token_id.toString();
×
127
        break;
×
128
      }
129
      case 'eth_native': {
130
        balanceSourceType = BalanceSourceType.ETHNative;
6✔
131
        chainId = thresholdData.source.evm_chain_id.toString();
6✔
132
        break;
6✔
133
      }
134
      case 'cosmos_native': {
135
        //balanceSourceType not used downstream by tbc other than EVM contracts, Osmosis works for all cosmos chains
136
        balanceSourceType = BalanceSourceType.CosmosNative;
1✔
137
        chainId = thresholdData.source.cosmos_chain_id;
1✔
138
        break;
1✔
139
      }
140
      case 'cw20': {
141
        balanceSourceType = BalanceSourceType.CW20;
×
142
        contractAddress = thresholdData.source.contract_address;
×
143
        chainId = thresholdData.source.cosmos_chain_id;
×
144
        break;
×
145
      }
146
      case 'cw721': {
147
        balanceSourceType = BalanceSourceType.CW721;
×
148
        contractAddress = thresholdData.source.contract_address;
×
149
        chainId = thresholdData.source.cosmos_chain_id;
×
150
        break;
×
151
      }
152
      default:
153
        break;
×
154
    }
155

156
    const balance = balances
19✔
157
      .filter((b) => b.options.balanceSourceType === balanceSourceType)
49✔
158
      .find((b) => {
159
        switch (b.options.balanceSourceType) {
19!
160
          case BalanceSourceType.ERC20:
161
          case BalanceSourceType.ERC721:
162
            return (
12✔
163
              b.options.sourceOptions.contractAddress == contractAddress &&
24✔
164
              b.options.sourceOptions.evmChainId.toString() === chainId
165
            );
166
          case BalanceSourceType.ERC1155:
167
            return (
×
168
              b.options.sourceOptions.contractAddress == contractAddress &&
×
169
              b.options.sourceOptions.evmChainId.toString() === chainId &&
170
              b.options.sourceOptions.tokenId.toString() === tokenId
171
            );
172
          case BalanceSourceType.ETHNative:
173
            return b.options.sourceOptions.evmChainId.toString() === chainId;
6✔
174
          case BalanceSourceType.CosmosNative:
175
            return b.options.sourceOptions.cosmosChainId.toString() === chainId;
1✔
176
          case BalanceSourceType.CW20:
177
          case BalanceSourceType.CW721:
178
            return (
×
179
              b.options.sourceOptions.contractAddress == contractAddress &&
×
180
              b.options.sourceOptions.cosmosChainId.toString() === chainId
181
            );
182
          case BalanceSourceType.SPL:
183
            return b.options.mintAddress == contractAddress;
×
184
          default:
185
            return null;
×
186
        }
187
      })?.balances[userAddress];
188

189
    if (typeof balance !== 'string') {
19!
190
      throw new Error(`Failed to get balance for address`);
×
191
    }
192

193
    const result = toBigInt(balance) > toBigInt(thresholdData.threshold);
19✔
194
    return {
19✔
195
      result,
196
      message: !result
19✔
197
        ? `User Balance of ${balance} below threshold ${thresholdData.threshold}`
198
        : 'pass',
199
    };
200
  } catch (error) {
201
    return {
×
202
      result: false,
203
      message: `Error: ${error instanceof Error ? error.message : error}`,
×
204
    };
205
  }
206
}
207

208
function _allowlistCheck(
209
  userAddress: string,
210
  allowlistData: AllowlistData,
211
): { result: boolean; message: string } {
212
  try {
×
213
    const result = allowlistData.allow.includes(userAddress);
×
214
    return {
×
215
      result,
216
      message: !result ? 'User Address not in Allowlist' : 'pass',
×
217
    };
218
  } catch (error) {
219
    return {
×
220
      result: false,
221
      message: `Error: ${error instanceof Error ? error.message : error}`,
×
222
    };
223
  }
224
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc