Skip to content

Commit

Permalink
Merge pull request #86 from aibtcdev/fix/sync-contract-templates
Browse files Browse the repository at this point in the history
Sync contract templates

Works on mainnet after a few tweaks! [Feel the fun!](https://explorer.hiro.so/address/SP168APDMNB7MAZB7HM4AGEB8JNN2R2SAF3G57WJ2?chain=mainnet)

*also this was just a test of the contracts, NFA*
  • Loading branch information
whoabuddy authored Jan 15, 2025
2 parents e8df9e0 + 88be183 commit a42b805
Show file tree
Hide file tree
Showing 18 changed files with 287 additions and 374 deletions.
74 changes: 40 additions & 34 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const ADDRESSES: NetworkAddressMap = {
STXCITY_COMPLETE_FEE: "ST295MNE41DC74QYCPRS8N37YYMC06N6Q3VQDZ6G1",
STXCITY_TOKEN_DEPLOYMENT_FEE: "ST295MNE41DC74QYCPRS8N37YYMC06N6Q3VQDZ6G1",
STXCITY_DEX_DEPLOYMENT_FEE: "ST295MNE41DC74QYCPRS8N37YYMC06N6Q3VQDZ6G1",
BITFLOW_CORE: "ST6BBNM7Q8GKDG2FMKRBZJCJ3SE4BWVC1YSF2XKD.xyk-core-v-1-2",
BITFLOW_CORE: "STTWD9SPRQVD3P733V89SV0P8RZRZNQADG034F0A.xyk-core-v-1-2",
BITFLOW_STX_TOKEN:
"ST295MNE41DC74QYCPRS8N37YYMC06N6Q3VQDZ6G1.token-stx-v-1-2",
BITFLOW_FEE: "ST295MNE41DC74QYCPRS8N37YYMC06N6Q3VQDZ6G1",
Expand Down Expand Up @@ -59,74 +59,78 @@ export const TRAITS: NetworkTraitsMap = {
"SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait",
SIP09: "SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait",
DAO_BASE:
"SPTWD9SPRQVD3P733V89SV0P8RZRZNQADJHHPME1.aibtcdev-dao-v1.aibtcdev-base-dao",
"SP29CK9990DQGE9RGTT1VEQTTYH8KY4E3JE5XP4EC.aibtcdev-dao-v1.aibtcdev-base-dao",
DAO_PROPOSAL:
"SPTWD9SPRQVD3P733V89SV0P8RZRZNQADJHHPME1.aibtcdev-dao-traits-v1.proposal",
"SP29CK9990DQGE9RGTT1VEQTTYH8KY4E3JE5XP4EC.aibtcdev-dao-traits-v1.proposal",
DAO_EXTENSION:
"SPTWD9SPRQVD3P733V89SV0P8RZRZNQADJHHPME1.aibtcdev-dao-traits-v1.extension",
"SP29CK9990DQGE9RGTT1VEQTTYH8KY4E3JE5XP4EC.aibtcdev-dao-traits-v1.extension",
DAO_ACTION_PROPOSALS:
"SPTWD9SPRQVD3P733V89SV0P8RZRZNQADJHHPME1.aibtcdev-dao-traits-v1.action-proposals",
"SP29CK9990DQGE9RGTT1VEQTTYH8KY4E3JE5XP4EC.aibtcdev-dao-traits-v1.action-proposals",
DAO_CORE_PROPOSALS:
"SPTWD9SPRQVD3P733V89SV0P8RZRZNQADJHHPME1.aibtcdev-dao-traits-v1.core-proposals",
"SP29CK9990DQGE9RGTT1VEQTTYH8KY4E3JE5XP4EC.aibtcdev-dao-traits-v1.core-proposals",
DAO_TREASURY:
"SPTWD9SPRQVD3P733V89SV0P8RZRZNQADJHHPME1.aibtcdev-dao-traits-v1.treasury",
"SP29CK9990DQGE9RGTT1VEQTTYH8KY4E3JE5XP4EC.aibtcdev-dao-traits-v1.treasury",
DAO_MESSAGING:
"SPTWD9SPRQVD3P733V89SV0P8RZRZNQADJHHPME1.aibtcdev-dao-traits-v1.messaging",
"SP29CK9990DQGE9RGTT1VEQTTYH8KY4E3JE5XP4EC.aibtcdev-dao-traits-v1.messaging",
DAO_BANK_ACCOUNT:
"SPTWD9SPRQVD3P733V89SV0P8RZRZNQADJHHPME1.aibtcdev-dao-traits-v1.bank-account",
"SP29CK9990DQGE9RGTT1VEQTTYH8KY4E3JE5XP4EC.aibtcdev-dao-traits-v1.bank-account",
DAO_RESOURCES:
"SPTWD9SPRQVD3P733V89SV0P8RZRZNQADJHHPME1.aibtcdev-dao-traits-v1.resources",
"SP29CK9990DQGE9RGTT1VEQTTYH8KY4E3JE5XP4EC.aibtcdev-dao-traits-v1.resources",
DAO_INVOICES:
"SPTWD9SPRQVD3P733V89SV0P8RZRZNQADJHHPME1.aibtcdev-dao-traits-v1.invoices",
"SP29CK9990DQGE9RGTT1VEQTTYH8KY4E3JE5XP4EC.aibtcdev-dao-traits-v1.invoices",
DAO_ACTION:
"SPTWD9SPRQVD3P733V89SV0P8RZRZNQADJHHPME1.aibtcdev-dao-traits-v1.action",
"SP29CK9990DQGE9RGTT1VEQTTYH8KY4E3JE5XP4EC.aibtcdev-dao-traits-v1.action",
DAO_TOKEN_DEX:
"SPTWD9SPRQVD3P733V89SV0P8RZRZNQADJHHPME1.aibtcdev-dao-traits-v1.token-dex",
"SP29CK9990DQGE9RGTT1VEQTTYH8KY4E3JE5XP4EC.aibtcdev-dao-traits-v1.token-dex",
DAO_TOKEN_OWNER:
"SPTWD9SPRQVD3P733V89SV0P8RZRZNQADJHHPME1.aibtcdev-dao-traits-v1.token-owner",
"SP29CK9990DQGE9RGTT1VEQTTYH8KY4E3JE5XP4EC.aibtcdev-dao-traits-v1.token-owner",
DAO_TOKEN:
"SPTWD9SPRQVD3P733V89SV0P8RZRZNQADJHHPME1.aibtcdev-dao-traits-v1.token",
"SP29CK9990DQGE9RGTT1VEQTTYH8KY4E3JE5XP4EC.aibtcdev-dao-traits-v1.token",
DAO_BITFLOW_POOL:
"SPTWD9SPRQVD3P733V89SV0P8RZRZNQADJHHPME1.aibtcdev-dao-traits-v1.bitflow-pool",
"SP29CK9990DQGE9RGTT1VEQTTYH8KY4E3JE5XP4EC.aibtcdev-dao-traits-v1.bitflow-pool",
BITFLOW_POOL:
"SM1793C4R5PZ4NS4VQ4WMP7SKKYVH8JZEWSZ9HCCR.xyk-pool-trait-v-1-2.xyk-pool-trait",
BITFLOW_SIP010:
"SM1793C4R5PZ4NS4VQ4WMP7SKKYVH8JZEWSZ9HCCR.sip-010-trait-ft-standard-v-1-1.sip-010-trait",
},
testnet: {
SIP10:
"ST6BBNM7Q8GKDG2FMKRBZJCJ3SE4BWVC1YSF2XKD.sip-010-trait-ft-standard.sip-010-trait",
SIP09: "ST6BBNM7Q8GKDG2FMKRBZJCJ3SE4BWVC1YSF2XKD.nft-trait.nft-trait",
"STTWD9SPRQVD3P733V89SV0P8RZRZNQADG034F0A.sip-010-trait-ft-standard.sip-010-trait",
SIP09: "STTWD9SPRQVD3P733V89SV0P8RZRZNQADG034F0A.nft-trait.nft-trait",
DAO_BASE:
"ST6BBNM7Q8GKDG2FMKRBZJCJ3SE4BWVC1YSF2XKD.aibtcdev-dao-v1.aibtcdev-base-dao",
"STTWD9SPRQVD3P733V89SV0P8RZRZNQADG034F0A.aibtcdev-dao-v1.aibtcdev-base-dao",
DAO_PROPOSAL:
"ST6BBNM7Q8GKDG2FMKRBZJCJ3SE4BWVC1YSF2XKD.aibtcdev-dao-traits-v1.proposal",
"STTWD9SPRQVD3P733V89SV0P8RZRZNQADG034F0A.aibtcdev-dao-traits-v1.proposal",
DAO_EXTENSION:
"ST6BBNM7Q8GKDG2FMKRBZJCJ3SE4BWVC1YSF2XKD.aibtcdev-dao-traits-v1.extension",
"STTWD9SPRQVD3P733V89SV0P8RZRZNQADG034F0A.aibtcdev-dao-traits-v1.extension",
DAO_ACTION_PROPOSALS:
"ST6BBNM7Q8GKDG2FMKRBZJCJ3SE4BWVC1YSF2XKD.aibtcdev-dao-traits-v1.action-proposals",
"STTWD9SPRQVD3P733V89SV0P8RZRZNQADG034F0A.aibtcdev-dao-traits-v1.action-proposals",
DAO_CORE_PROPOSALS:
"ST6BBNM7Q8GKDG2FMKRBZJCJ3SE4BWVC1YSF2XKD.aibtcdev-dao-traits-v1.core-proposals",
"STTWD9SPRQVD3P733V89SV0P8RZRZNQADG034F0A.aibtcdev-dao-traits-v1.core-proposals",
DAO_TREASURY:
"ST6BBNM7Q8GKDG2FMKRBZJCJ3SE4BWVC1YSF2XKD.aibtcdev-dao-traits-v1.treasury",
"STTWD9SPRQVD3P733V89SV0P8RZRZNQADG034F0A.aibtcdev-dao-traits-v1.treasury",
DAO_MESSAGING:
"ST6BBNM7Q8GKDG2FMKRBZJCJ3SE4BWVC1YSF2XKD.aibtcdev-dao-traits-v1.messaging",
"STTWD9SPRQVD3P733V89SV0P8RZRZNQADG034F0A.aibtcdev-dao-traits-v1.messaging",
DAO_BANK_ACCOUNT:
"ST6BBNM7Q8GKDG2FMKRBZJCJ3SE4BWVC1YSF2XKD.aibtcdev-dao-traits-v1.bank-account",
"STTWD9SPRQVD3P733V89SV0P8RZRZNQADG034F0A.aibtcdev-dao-traits-v1.bank-account",
DAO_RESOURCES:
"ST6BBNM7Q8GKDG2FMKRBZJCJ3SE4BWVC1YSF2XKD.aibtcdev-dao-traits-v1.resources",
"STTWD9SPRQVD3P733V89SV0P8RZRZNQADG034F0A.aibtcdev-dao-traits-v1.resources",
DAO_INVOICES:
"ST6BBNM7Q8GKDG2FMKRBZJCJ3SE4BWVC1YSF2XKD.aibtcdev-dao-traits-v1.invoices",
"STTWD9SPRQVD3P733V89SV0P8RZRZNQADG034F0A.aibtcdev-dao-traits-v1.invoices",
DAO_ACTION:
"ST6BBNM7Q8GKDG2FMKRBZJCJ3SE4BWVC1YSF2XKD.aibtcdev-dao-traits-v1.action",
"STTWD9SPRQVD3P733V89SV0P8RZRZNQADG034F0A.aibtcdev-dao-traits-v1.action",
DAO_TOKEN_DEX:
"ST6BBNM7Q8GKDG2FMKRBZJCJ3SE4BWVC1YSF2XKD.aibtcdev-dao-traits-v1.token-dex",
"STTWD9SPRQVD3P733V89SV0P8RZRZNQADG034F0A.aibtcdev-dao-traits-v1.token-dex",
DAO_TOKEN_OWNER:
"ST6BBNM7Q8GKDG2FMKRBZJCJ3SE4BWVC1YSF2XKD.aibtcdev-dao-traits-v1.token-owner",
"STTWD9SPRQVD3P733V89SV0P8RZRZNQADG034F0A.aibtcdev-dao-traits-v1.token-owner",
DAO_TOKEN:
"ST6BBNM7Q8GKDG2FMKRBZJCJ3SE4BWVC1YSF2XKD.aibtcdev-dao-traits-v1.token",
"STTWD9SPRQVD3P733V89SV0P8RZRZNQADG034F0A.aibtcdev-dao-traits-v1.token",
DAO_BITFLOW_POOL:
"ST6BBNM7Q8GKDG2FMKRBZJCJ3SE4BWVC1YSF2XKD.aibtcdev-dao-traits-v1.bitflow-pool",
"STTWD9SPRQVD3P733V89SV0P8RZRZNQADG034F0A.aibtcdev-dao-traits-v1.bitflow-pool",
BITFLOW_POOL:
"ST6BBNM7Q8GKDG2FMKRBZJCJ3SE4BWVC1YSF2XKD.xyk-pool-trait-v-1-2.xyk-pool-trait",
"STTWD9SPRQVD3P733V89SV0P8RZRZNQADG034F0A.xyk-pool-trait-v-1-2.xyk-pool-trait",
BITFLOW_SIP010:
"STTWD9SPRQVD3P733V89SV0P8RZRZNQADG034F0A.sip-010-trait-ft-standard.sip-010-trait",
},
devnet: {
SIP10: "",
Expand All @@ -147,6 +151,7 @@ export const TRAITS: NetworkTraitsMap = {
DAO_TOKEN: "",
DAO_BITFLOW_POOL: "",
BITFLOW_POOL: "",
BITFLOW_SIP010: "",
},
mocknet: {
SIP10: "",
Expand All @@ -167,5 +172,6 @@ export const TRAITS: NetworkTraitsMap = {
DAO_TOKEN: "",
DAO_BITFLOW_POOL: "",
BITFLOW_POOL: "",
BITFLOW_SIP010: "",
},
};
7 changes: 6 additions & 1 deletion src/stacks-contracts/deploy-dao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,14 @@ async function main() {

// Step 2 - generate remaining dao contracts

// set dao manifest, passed to proposal for dao construction
const daoManifest =
"This is where the DAO can put it's mission, purpose, and goals.";

const contracts = contractGenerator.generateDaoContracts(
senderAddress,
tokenSymbol
tokenSymbol,
daoManifest
);

// Sort contracts to ensure DAO_PROPOSAL_BOOTSTRAP is last
Expand Down
2 changes: 1 addition & 1 deletion src/stacks-contracts/services/contract-deployer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class ContractDeployer {
anchorMode: AnchorMode.Any,
postConditionMode: PostConditionMode.Allow,
nonce: nonce !== undefined ? nonce : undefined,
fee: BigInt(100_000), // 0.1 STX
fee: BigInt(500_000), // 0.5 STX
};

const transaction = await makeContractDeploy(deployOptions);
Expand Down
63 changes: 46 additions & 17 deletions src/stacks-contracts/services/contract-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,19 @@ export class ContractGenerator {
}

// extension: aibtc-action-proposals
private generateActionProposalsContract(daoContractAddress: string): string {
private generateActionProposalsContract(
daoContractAddress: string,
tokenContractAddress: string,
tokenDexContractAddress: string,
tokenPoolContractAddress: string,
treasuryContractAddress: string
): string {
const data = {
dao_contract_address: daoContractAddress,
token_contract_address: tokenContractAddress,
token_dex_contract_address: tokenDexContractAddress,
token_pool_contract_address: tokenPoolContractAddress,
treasury_contract_address: treasuryContractAddress,
extension_trait: getTraitReference(this.network, "DAO_EXTENSION"),
action_proposals_trait: getTraitReference(
this.network,
Expand Down Expand Up @@ -74,7 +84,7 @@ export class ContractGenerator {
"DAO_BITFLOW_POOL"
),
bitflow_pool_trait: getTraitReference(this.network, "BITFLOW_POOL"),
sip10_trait: getTraitReference(this.network, "SIP10"),
sip10_trait: getTraitReference(this.network, "BITFLOW_SIP010"),
bitflow_xyk_core_address: getAddressReference(
this.network,
"BITFLOW_CORE"
Expand All @@ -86,9 +96,19 @@ export class ContractGenerator {
}

// extension: aibtc-core-proposals
private generateCorePropasalsContract(daoContractAddress: string): string {
private generateCorePropasalsContract(
daoContractAddress: string,
tokenContractAddress: string,
tokenDexContractAddress: string,
tokenPoolContractAddress: string,
treasuryContractAddress: string
): string {
const data = {
dao_contract_address: daoContractAddress,
token_contract_address: tokenContractAddress,
token_dex_contract_address: tokenDexContractAddress,
token_pool_contract_address: tokenPoolContractAddress,
treasury_contract_address: treasuryContractAddress,
extension_trait: getTraitReference(this.network, "DAO_EXTENSION"),
core_proposal_trait: getTraitReference(
this.network,
Expand Down Expand Up @@ -158,7 +178,7 @@ export class ContractGenerator {
bitflow_core_contract: getAddressReference(this.network, "BITFLOW_CORE"),
sip10_trait: getTraitReference(this.network, "SIP10"),
token_dex_trait: getTraitReference(this.network, "DAO_TOKEN_DEX"),
token_contract: tokenContract,
token_contract_address: tokenContract,
pool_contract: poolContract,
bitflow_fee_address: getAddressReference(this.network, "BITFLOW_FEE"),
bitflow_stx_token_address: getAddressReference(
Expand All @@ -184,14 +204,11 @@ export class ContractGenerator {
// extension: aibtc-token-owner
private generateTokenOwnerContract(
daoContractAddress: string,
tokenSymbol: string
tokenContractAddress: string
): string {
const tokenContract = `${
this.senderAddress
}.${tokenSymbol.toLowerCase()}-stxcity`;
const data = {
dao_contract_address: daoContractAddress,
token_contract: tokenContract,
token_contract_address: tokenContractAddress,
extension_trait: getTraitReference(this.network, "DAO_EXTENSION"),
token_owner_trait: getTraitReference(this.network, "DAO_TOKEN_OWNER"),
creator: this.senderAddress,
Expand Down Expand Up @@ -432,22 +449,26 @@ export class ContractGenerator {
// generated at runtime based on script parameters
generateDaoContracts(
senderAddress: string,
tokenSymbol: string
tokenSymbol: string,
daoManifest: string
): GeneratedDaoContracts {
// set dao manifest, passed to proposal for dao construction
const daoManifest = "This is where the dao manifest would go";

// get contract names using token symbol
const contractNames = generateContractNames(tokenSymbol);

const generateContractPrincipal = (
contractType: ContractType | ContractActionType
) => `${senderAddress}.${contractNames[contractType]}`;

// construct token contract address
// construct token related contract addresses
const tokenContractAddress = generateContractPrincipal(
ContractType.DAO_TOKEN
);
const tokenDexContractAddress = generateContractPrincipal(
ContractType.DAO_TOKEN_DEX
);
const tokenPoolContractAddress = generateContractPrincipal(
ContractType.DAO_BITFLOW_POOL
);

// construct base dao contract address
const baseDaoContractAddress = generateContractPrincipal(
Expand Down Expand Up @@ -521,14 +542,22 @@ export class ContractGenerator {
baseDaoContractAddress
);
const coreProposalsContract = this.generateCorePropasalsContract(
baseDaoContractAddress
baseDaoContractAddress,
tokenContractAddress,
tokenDexContractAddress,
tokenPoolContractAddress,
treasuryContractAddress
);
const actionProposalsContract = this.generateActionProposalsContract(
baseDaoContractAddress
baseDaoContractAddress,
tokenContractAddress,
tokenDexContractAddress,
tokenPoolContractAddress,
treasuryContractAddress
);
const tokenOwnerContract = this.generateTokenOwnerContract(
baseDaoContractAddress,
tokenSymbol
tokenContractAddress
);

// generate action extension contract code
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@

(define-private (is-dao-or-extension)
(ok (asserts! (or (is-eq tx-sender '<%= it.dao_contract_address %>)
(contract-call? '<%= it.dao_contract_address %> is-extension contract-caller)) ERR_NOT_DAO_OR_EXTENSION
(contract-call? '<%= it.dao_contract_address %> is-extension contract-caller)) ERR_UNAUTHORIZED
))
)
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@

(define-private (is-dao-or-extension)
(ok (asserts! (or (is-eq tx-sender '<%= it.dao_contract_address %>)
(contract-call? '<%= it.dao_contract_address %> is-extension contract-caller)) ERR_NOT_DAO_OR_EXTENSION
(contract-call? '<%= it.dao_contract_address %> is-extension contract-caller)) ERR_UNAUTHORIZED
))
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@
(define-public (callback (sender principal) (memo (buff 34))) (ok true))

(define-public (run (parameters (buff 2048)))
(begin
(let
(
(message (unwrap! (from-consensus-buff? (string-ascii 2043) parameters) ERR_INVALID_PARAMS))
)
(try! (is-dao-or-extension))
(contract-call? '<%= it.messaging_contract_address %> send (unwrap! (from-consensus-buff? (string-ascii 2043) parameters) ERR_INVALID_PARAMS) true)
(contract-call? '<%= it.messaging_contract_address %> send message true)
)
)

(define-private (is-dao-or-extension)
(ok (asserts! (or (is-eq tx-sender '<%= it.dao_contract_address %>)
(contract-call? '<%= it.dao_contract_address %> is-extension contract-caller)) ERR_NOT_DAO_OR_EXTENSION
(contract-call? '<%= it.dao_contract_address %> is-extension contract-caller)) ERR_UNAUTHORIZED
))
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@
(define-public (callback (sender principal) (memo (buff 34))) (ok true))

(define-public (run (parameters (buff 2048)))
(begin
(let
(
(accountHolder (unwrap! (from-consensus-buff? principal parameters) ERR_INVALID_PARAMS))
)
(try! (is-dao-or-extension))
(contract-call? .aibtc-bank-account set-account-holder (unwrap! (from-consensus-buff? principal parameters) ERR_INVALID_PARAMS))
(contract-call? '<%= it.bank_account_contract_address %> set-account-holder accountHolder)
)
)

(define-private (is-dao-or-extension)
(ok (asserts! (or (is-eq tx-sender '<%= it.dao_contract_address %>)
(contract-call? '<%= it.dao_contract_address %> is-extension contract-caller)) ERR_NOT_DAO_OR_EXTENSION
(contract-call? '<%= it.dao_contract_address %> is-extension contract-caller)) ERR_UNAUTHORIZED
))
)
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@

(define-private (is-dao-or-extension)
(ok (asserts! (or (is-eq tx-sender '<%= it.dao_contract_address %>)
(contract-call? '<%= it.dao_contract_address %> is-extension contract-caller)) ERR_NOT_DAO_OR_EXTENSION
(contract-call? '<%= it.dao_contract_address %> is-extension contract-caller)) ERR_UNAUTHORIZED
))
)
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@

(define-private (is-dao-or-extension)
(ok (asserts! (or (is-eq tx-sender '<%= it.dao_contract_address %>)
(contract-call? '<%= it.dao_contract_address %> is-extension contract-caller)) ERR_NOT_DAO_OR_EXTENSION
(contract-call? '<%= it.dao_contract_address %> is-extension contract-caller)) ERR_UNAUTHORIZED
))
)
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@

(define-private (is-dao-or-extension)
(ok (asserts! (or (is-eq tx-sender '<%= it.dao_contract_address %>)
(contract-call? '<%= it.dao_contract_address %> is-extension contract-caller)) ERR_NOT_DAO_OR_EXTENSION
(contract-call? '<%= it.dao_contract_address %> is-extension contract-caller)) ERR_UNAUTHORIZED
))
)
Loading

0 comments on commit a42b805

Please sign in to comment.