mirror of
https://github.com/vacp2p/rln-interep-contract.git
synced 2026-01-08 21:27:58 -05:00
style: fmt
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
istanbulReporter: ['lcov'],
|
||||
istanbulFolder: 'coverage',
|
||||
}
|
||||
istanbulReporter: ["lcov"],
|
||||
istanbulFolder: "coverage",
|
||||
};
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
export const devNets = [
|
||||
'hardhat',
|
||||
'localhost',
|
||||
'goerli',
|
||||
]
|
||||
export const devNets = ["hardhat", "localhost", "goerli"];
|
||||
|
||||
export const prodNets = ['mainnet']
|
||||
export const prodNets = ["mainnet"];
|
||||
|
||||
export const isDevNet = (networkName: string) => devNets.includes(networkName)
|
||||
export const isDevNet = (networkName: string) => devNets.includes(networkName);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// barrel for all other utils
|
||||
export * from './interep-utils';
|
||||
export * from './chain-utils';
|
||||
export * from "./interep-utils";
|
||||
export * from "./chain-utils";
|
||||
|
||||
@@ -1,33 +1,42 @@
|
||||
import {utils} from "ethers";
|
||||
import { utils } from "ethers";
|
||||
|
||||
export const sToBytes32 = (str: string): string => {
|
||||
return utils.formatBytes32String(str);
|
||||
}
|
||||
return utils.formatBytes32String(str);
|
||||
};
|
||||
|
||||
export const SNARK_SCALAR_FIELD = BigInt(
|
||||
"21888242871839275222246405745257275088548364400416034343698204186575808495617"
|
||||
)
|
||||
"21888242871839275222246405745257275088548364400416034343698204186575808495617"
|
||||
);
|
||||
|
||||
export const createGroupId = (provider: string, name: string): bigint => {
|
||||
const providerBytes = sToBytes32(provider);
|
||||
const nameBytes = sToBytes32(name);
|
||||
return BigInt(utils.solidityKeccak256(["bytes32", "bytes32"], [providerBytes, nameBytes])) % SNARK_SCALAR_FIELD
|
||||
}
|
||||
const providerBytes = sToBytes32(provider);
|
||||
const nameBytes = sToBytes32(name);
|
||||
return (
|
||||
BigInt(
|
||||
utils.solidityKeccak256(
|
||||
["bytes32", "bytes32"],
|
||||
[providerBytes, nameBytes]
|
||||
)
|
||||
) % SNARK_SCALAR_FIELD
|
||||
);
|
||||
};
|
||||
|
||||
const providers = ['github', 'twitter', 'reddit'];
|
||||
const tiers = ['bronze', 'silver', 'gold'];
|
||||
const providers = ["github", "twitter", "reddit"];
|
||||
const tiers = ["bronze", "silver", "gold"];
|
||||
|
||||
export const getGroups = () => {
|
||||
return providers.flatMap(provider => tiers.map(tier => {
|
||||
return {
|
||||
provider: sToBytes32(provider),
|
||||
name: sToBytes32(tier),
|
||||
root: 1,
|
||||
depth: 10,
|
||||
}
|
||||
}));
|
||||
}
|
||||
return providers.flatMap((provider) =>
|
||||
tiers.map((tier) => {
|
||||
return {
|
||||
provider: sToBytes32(provider),
|
||||
name: sToBytes32(tier),
|
||||
root: 1,
|
||||
depth: 10,
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const getValidGroups = () => {
|
||||
return getGroups().filter(group => group.name !== sToBytes32('bronze'));
|
||||
}
|
||||
return getGroups().filter((group) => group.name !== sToBytes32("bronze"));
|
||||
};
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import {HardhatRuntimeEnvironment} from 'hardhat/types';
|
||||
import {DeployFunction} from 'hardhat-deploy/types';
|
||||
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||
import { DeployFunction } from "hardhat-deploy/types";
|
||||
|
||||
const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
|
||||
const {deployments, getUnnamedAccounts} = hre;
|
||||
const {deploy} = deployments;
|
||||
|
||||
const [deployer] = await getUnnamedAccounts();
|
||||
|
||||
await deploy('PoseidonHasher', {
|
||||
from: deployer,
|
||||
log: true,
|
||||
});
|
||||
const { deployments, getUnnamedAccounts } = hre;
|
||||
const { deploy } = deployments;
|
||||
|
||||
const [deployer] = await getUnnamedAccounts();
|
||||
|
||||
await deploy("PoseidonHasher", {
|
||||
from: deployer,
|
||||
log: true,
|
||||
});
|
||||
};
|
||||
export default func;
|
||||
func.tags = ['PoseidonHasher'];
|
||||
func.tags = ["PoseidonHasher"];
|
||||
|
||||
@@ -1,35 +1,33 @@
|
||||
import {HardhatRuntimeEnvironment} from 'hardhat/types';
|
||||
import {DeployFunction} from 'hardhat-deploy/types';
|
||||
import {
|
||||
getGroups,
|
||||
isDevNet
|
||||
} from '../common';
|
||||
|
||||
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||
import { DeployFunction } from "hardhat-deploy/types";
|
||||
import { getGroups, isDevNet } from "../common";
|
||||
|
||||
const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
|
||||
const {deployments, getUnnamedAccounts} = hre;
|
||||
const {deploy} = deployments;
|
||||
|
||||
const [deployer] = await getUnnamedAccounts();
|
||||
|
||||
const interepTest = await deploy('InterepTest', {
|
||||
from: deployer,
|
||||
log: true,
|
||||
args: [],
|
||||
});
|
||||
const { deployments, getUnnamedAccounts } = hre;
|
||||
const { deploy } = deployments;
|
||||
|
||||
const contract = await hre.ethers.getContractAt('InterepTest', interepTest.address);
|
||||
const groups = getGroups();
|
||||
const groupInsertionTx = await contract.updateGroups(groups);
|
||||
await groupInsertionTx.wait();
|
||||
|
||||
const [deployer] = await getUnnamedAccounts();
|
||||
|
||||
const interepTest = await deploy("InterepTest", {
|
||||
from: deployer,
|
||||
log: true,
|
||||
args: [],
|
||||
});
|
||||
|
||||
const contract = await hre.ethers.getContractAt(
|
||||
"InterepTest",
|
||||
interepTest.address
|
||||
);
|
||||
const groups = getGroups();
|
||||
const groupInsertionTx = await contract.updateGroups(groups);
|
||||
await groupInsertionTx.wait();
|
||||
};
|
||||
export default func;
|
||||
func.tags = ['InterepTest'];
|
||||
func.tags = ["InterepTest"];
|
||||
// skip when running on mainnet
|
||||
func.skip = async (hre: HardhatRuntimeEnvironment) => {
|
||||
if (isDevNet(hre.network.name)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (isDevNet(hre.network.name)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import {HardhatRuntimeEnvironment} from 'hardhat/types';
|
||||
import {DeployFunction} from 'hardhat-deploy/types';
|
||||
import {getValidGroups, isDevNet} from '../common';
|
||||
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||
import { DeployFunction } from "hardhat-deploy/types";
|
||||
import { getValidGroups, isDevNet } from "../common";
|
||||
|
||||
const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
|
||||
const {deployments, getUnnamedAccounts} = hre;
|
||||
const {deploy} = deployments;
|
||||
|
||||
const [deployer] = await getUnnamedAccounts();
|
||||
const { deployments, getUnnamedAccounts } = hre;
|
||||
const { deploy } = deployments;
|
||||
|
||||
if (!isDevNet(hre.network.name)) {
|
||||
throw new Error('Interep not deployed on mainnet yet.')
|
||||
}
|
||||
const interepAddress = isDevNet(hre.network.name) ? (await deployments.get('InterepTest')).address : '0x0000000000000000000000000000000000000000';
|
||||
|
||||
await deploy('ValidGroupStorage', {
|
||||
from: deployer,
|
||||
log: true,
|
||||
args: [interepAddress, getValidGroups()]
|
||||
});
|
||||
const [deployer] = await getUnnamedAccounts();
|
||||
|
||||
if (!isDevNet(hre.network.name)) {
|
||||
throw new Error("Interep not deployed on mainnet yet.");
|
||||
}
|
||||
const interepAddress = isDevNet(hre.network.name)
|
||||
? (await deployments.get("InterepTest")).address
|
||||
: "0x0000000000000000000000000000000000000000";
|
||||
|
||||
await deploy("ValidGroupStorage", {
|
||||
from: deployer,
|
||||
log: true,
|
||||
args: [interepAddress, getValidGroups()],
|
||||
});
|
||||
};
|
||||
export default func;
|
||||
func.tags = ['ValidGroupStorage'];
|
||||
func.dependencies = ['InterepTest'];
|
||||
func.tags = ["ValidGroupStorage"];
|
||||
func.dependencies = ["InterepTest"];
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
import {HardhatRuntimeEnvironment} from 'hardhat/types';
|
||||
import {DeployFunction} from 'hardhat-deploy/types';
|
||||
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||
import { DeployFunction } from "hardhat-deploy/types";
|
||||
|
||||
const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
|
||||
const {deployments, getUnnamedAccounts} = hre;
|
||||
const {deploy} = deployments;
|
||||
|
||||
const [deployer] = await getUnnamedAccounts();
|
||||
const { deployments, getUnnamedAccounts } = hre;
|
||||
const { deploy } = deployments;
|
||||
|
||||
const poseidonHasherAddress = (await deployments.get('PoseidonHasher')).address;
|
||||
const validGroupStorageAddress = (await deployments.get('ValidGroupStorage')).address;
|
||||
|
||||
await deploy('RLN', {
|
||||
from: deployer,
|
||||
log: true,
|
||||
args: [1000000000000000, 20, poseidonHasherAddress, validGroupStorageAddress]
|
||||
});
|
||||
const [deployer] = await getUnnamedAccounts();
|
||||
|
||||
const poseidonHasherAddress = (await deployments.get("PoseidonHasher"))
|
||||
.address;
|
||||
const validGroupStorageAddress = (await deployments.get("ValidGroupStorage"))
|
||||
.address;
|
||||
|
||||
await deploy("RLN", {
|
||||
from: deployer,
|
||||
log: true,
|
||||
args: [
|
||||
1000000000000000,
|
||||
20,
|
||||
poseidonHasherAddress,
|
||||
validGroupStorageAddress,
|
||||
],
|
||||
});
|
||||
};
|
||||
export default func;
|
||||
func.tags = ['RLN'];
|
||||
func.dependencies = ['PoseidonHasher', 'ValidGroupStorage'];
|
||||
func.tags = ["RLN"];
|
||||
func.dependencies = ["PoseidonHasher", "ValidGroupStorage"];
|
||||
|
||||
@@ -10,32 +10,35 @@ import "hardhat-gas-reporter";
|
||||
import "solidity-coverage";
|
||||
|
||||
dotenv.config();
|
||||
const {GOERLI_URL,PRIVATE_KEY} = process.env;
|
||||
const { GOERLI_URL, PRIVATE_KEY } = process.env;
|
||||
|
||||
const getNetworkConfig = (): NetworksUserConfig | undefined => {
|
||||
if (GOERLI_URL && PRIVATE_KEY) {
|
||||
return {
|
||||
goerli: {
|
||||
url: GOERLI_URL,
|
||||
accounts: [PRIVATE_KEY],
|
||||
}
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (GOERLI_URL && PRIVATE_KEY) {
|
||||
return {
|
||||
goerli: {
|
||||
url: GOERLI_URL,
|
||||
accounts: [PRIVATE_KEY],
|
||||
},
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// You need to export an object to set up your config
|
||||
// Go to https://hardhat.org/config/ to learn more
|
||||
|
||||
const config: HardhatUserConfig = {
|
||||
solidity: {
|
||||
compilers: [{
|
||||
version: "0.8.4",
|
||||
}, {
|
||||
version: "0.8.15"
|
||||
}],
|
||||
compilers: [
|
||||
{
|
||||
version: "0.8.4",
|
||||
},
|
||||
{
|
||||
version: "0.8.15",
|
||||
},
|
||||
],
|
||||
},
|
||||
networks: getNetworkConfig()
|
||||
networks: getNetworkConfig(),
|
||||
};
|
||||
|
||||
export default config;
|
||||
export default config;
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"deploy": "hardhat run scripts/deploy.ts --network",
|
||||
"deploy:goerli": "yarn deploy goerli",
|
||||
"deploy:localhost": "yarn deploy localhost",
|
||||
"coverage": "hardhat coverage"
|
||||
"coverage": "hardhat coverage",
|
||||
"fmt": "prettier --write \"**/*.{js,ts}\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@interep/contracts": "0.6.0",
|
||||
@@ -24,6 +25,7 @@
|
||||
"hardhat": "^2.9.9",
|
||||
"hardhat-deploy": "0.11.20",
|
||||
"hardhat-gas-reporter": "^1.0.8",
|
||||
"prettier": "^2.8.0",
|
||||
"solidity-coverage": "^0.7.21",
|
||||
"ts-node": "^10.8.1",
|
||||
"typescript": "^4.7.4"
|
||||
@@ -31,4 +33,4 @@
|
||||
"dependencies": {
|
||||
"dotenv": "^16.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,14 @@ describe("PoseidonHasher", () => {
|
||||
|
||||
it("should hash correctly", async function () {
|
||||
const poseidonHasher = await ethers.getContract("PoseidonHasher");
|
||||
|
||||
|
||||
// We test hashing for a random number
|
||||
const hash = await poseidonHasher.hash("19014214495641488759237505126948346942972912379615652741039992445865937985820");
|
||||
|
||||
expect(hash._hex).to.eql("0x0c3ac305f6a4fe9bfeb3eba978bc876e2a99208b8b56c80160cfb54ba8f02368");
|
||||
const hash = await poseidonHasher.hash(
|
||||
"19014214495641488759237505126948346942972912379615652741039992445865937985820"
|
||||
);
|
||||
|
||||
expect(hash._hex).to.eql(
|
||||
"0x0c3ac305f6a4fe9bfeb3eba978bc876e2a99208b8b56c80160cfb54ba8f02368"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
67
test/rln.ts
67
test/rln.ts
@@ -4,53 +4,66 @@ import { ethers, deployments } from "hardhat";
|
||||
describe("RLN", () => {
|
||||
beforeEach(async () => {
|
||||
await deployments.fixture(["RLN"]);
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
it("should register new memberships", async () => {
|
||||
const rln = await ethers.getContract("RLN", ethers.provider.getSigner(0));
|
||||
|
||||
|
||||
const price = await rln.MEMBERSHIP_DEPOSIT();
|
||||
|
||||
// A valid pair of (id_secret, id_commitment) generated in rust
|
||||
const idCommitment = "0x0c3ac305f6a4fe9bfeb3eba978bc876e2a99208b8b56c80160cfb54ba8f02368"
|
||||
const idCommitment =
|
||||
"0x0c3ac305f6a4fe9bfeb3eba978bc876e2a99208b8b56c80160cfb54ba8f02368";
|
||||
|
||||
|
||||
const registerTx = await rln['register(uint256)'](idCommitment, {value: price});
|
||||
const registerTx = await rln["register(uint256)"](idCommitment, {
|
||||
value: price,
|
||||
});
|
||||
const txRegisterReceipt = await registerTx.wait();
|
||||
|
||||
const pubkey = txRegisterReceipt.events[0].args.pubkey;
|
||||
const pubkey = txRegisterReceipt.events[0].args.pubkey;
|
||||
|
||||
// We ensure the registered id_commitment is the one we passed
|
||||
expect(pubkey.toHexString() === idCommitment, "registered commitment doesn't match passed commitment");
|
||||
expect(
|
||||
pubkey.toHexString() === idCommitment,
|
||||
"registered commitment doesn't match passed commitment"
|
||||
);
|
||||
});
|
||||
|
||||
it("should withdraw membership", async () => {
|
||||
const rln = await ethers.getContract("RLN", ethers.provider.getSigner(0));
|
||||
|
||||
|
||||
const price = await rln.MEMBERSHIP_DEPOSIT();
|
||||
|
||||
// A valid pair of (id_secret, id_commitment) generated in rust
|
||||
const idSecret = "0x2a09a9fd93c590c26b91effbb2499f07e8f7aa12e2b4940a3aed2411cb65e11c"
|
||||
const idCommitment = "0x0c3ac305f6a4fe9bfeb3eba978bc876e2a99208b8b56c80160cfb54ba8f02368"
|
||||
const idSecret =
|
||||
"0x2a09a9fd93c590c26b91effbb2499f07e8f7aa12e2b4940a3aed2411cb65e11c";
|
||||
const idCommitment =
|
||||
"0x0c3ac305f6a4fe9bfeb3eba978bc876e2a99208b8b56c80160cfb54ba8f02368";
|
||||
|
||||
|
||||
const registerTx = await rln['register(uint256)'](idCommitment, {value: price});
|
||||
const registerTx = await rln["register(uint256)"](idCommitment, {
|
||||
value: price,
|
||||
});
|
||||
const txRegisterReceipt = await registerTx.wait();
|
||||
|
||||
const treeIndex = txRegisterReceipt.events[0].args.index;
|
||||
const treeIndex = txRegisterReceipt.events[0].args.index;
|
||||
|
||||
// We withdraw our id_commitment
|
||||
const receiverAddress = "0x000000000000000000000000000000000000dead";
|
||||
const withdrawTx = await rln.withdraw(idSecret, treeIndex, receiverAddress);
|
||||
|
||||
const txWithdrawReceipt = await withdrawTx.wait();
|
||||
|
||||
const withdrawalPk = txWithdrawReceipt.events[0].args.pubkey;
|
||||
const withdrawalTreeIndex = txWithdrawReceipt.events[0].args.index;
|
||||
|
||||
// We ensure the registered id_commitment is the one we passed and that the index is the same
|
||||
expect(withdrawalPk.toHexString() === idCommitment, "withdraw commitment doesn't match registered commitment");
|
||||
expect(withdrawalTreeIndex.toHexString() === treeIndex.toHexString(), "withdraw index doesn't match registered index");
|
||||
})
|
||||
});
|
||||
const receiverAddress = "0x000000000000000000000000000000000000dead";
|
||||
const withdrawTx = await rln.withdraw(idSecret, treeIndex, receiverAddress);
|
||||
|
||||
const txWithdrawReceipt = await withdrawTx.wait();
|
||||
|
||||
const withdrawalPk = txWithdrawReceipt.events[0].args.pubkey;
|
||||
const withdrawalTreeIndex = txWithdrawReceipt.events[0].args.index;
|
||||
|
||||
// We ensure the registered id_commitment is the one we passed and that the index is the same
|
||||
expect(
|
||||
withdrawalPk.toHexString() === idCommitment,
|
||||
"withdraw commitment doesn't match registered commitment"
|
||||
);
|
||||
expect(
|
||||
withdrawalTreeIndex.toHexString() === treeIndex.toHexString(),
|
||||
"withdraw index doesn't match registered index"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,33 +1,42 @@
|
||||
import {expect} from "chai";
|
||||
import {ethers, deployments} from "hardhat";
|
||||
import {sToBytes32, createGroupId} from '../common';
|
||||
|
||||
import { expect } from "chai";
|
||||
import { ethers, deployments } from "hardhat";
|
||||
import { sToBytes32, createGroupId } from "../common";
|
||||
|
||||
describe("Valid Group Storage", () => {
|
||||
beforeEach(async () => {
|
||||
await deployments.fixture(['ValidGroupStorage']);
|
||||
})
|
||||
beforeEach(async () => {
|
||||
await deployments.fixture(["ValidGroupStorage"]);
|
||||
});
|
||||
|
||||
it('should not deploy if an invalid group is passed in constructor', async () => {
|
||||
const interepTest = await ethers.getContract('InterepTest');
|
||||
const interepAddress = interepTest.address;
|
||||
|
||||
const ValidGroupStorage = await ethers.getContractFactory("ValidGroupStorage");
|
||||
expect(ValidGroupStorage.deploy(interepAddress, [{
|
||||
provider: sToBytes32('github'),
|
||||
name: sToBytes32('diamond'),
|
||||
}])).to.be.revertedWith("[ValidGroupStorage] Invalid group");
|
||||
})
|
||||
it("should not deploy if an invalid group is passed in constructor", async () => {
|
||||
const interepTest = await ethers.getContract("InterepTest");
|
||||
const interepAddress = interepTest.address;
|
||||
|
||||
it("should return true for valid group", async () => {
|
||||
const validGroupStorage = await ethers.getContract('ValidGroupStorage');
|
||||
const valid = await validGroupStorage.isValidGroup(createGroupId('github', 'silver'));
|
||||
expect(valid).to.be.true;
|
||||
});
|
||||
const ValidGroupStorage = await ethers.getContractFactory(
|
||||
"ValidGroupStorage"
|
||||
);
|
||||
expect(
|
||||
ValidGroupStorage.deploy(interepAddress, [
|
||||
{
|
||||
provider: sToBytes32("github"),
|
||||
name: sToBytes32("diamond"),
|
||||
},
|
||||
])
|
||||
).to.be.revertedWith("[ValidGroupStorage] Invalid group");
|
||||
});
|
||||
|
||||
it("should return false for invalid group", async () => {
|
||||
const validGroupStorage = await ethers.getContract('ValidGroupStorage');
|
||||
const valid = await validGroupStorage.isValidGroup(createGroupId('github', 'bronze'));
|
||||
expect(valid).to.be.false;
|
||||
});
|
||||
})
|
||||
it("should return true for valid group", async () => {
|
||||
const validGroupStorage = await ethers.getContract("ValidGroupStorage");
|
||||
const valid = await validGroupStorage.isValidGroup(
|
||||
createGroupId("github", "silver")
|
||||
);
|
||||
expect(valid).to.be.true;
|
||||
});
|
||||
|
||||
it("should return false for invalid group", async () => {
|
||||
const validGroupStorage = await ethers.getContract("ValidGroupStorage");
|
||||
const valid = await validGroupStorage.isValidGroup(
|
||||
createGroupId("github", "bronze")
|
||||
);
|
||||
expect(valid).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6970,7 +6970,7 @@ prepend-http@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
||||
integrity sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==
|
||||
|
||||
prettier@^2.1.2:
|
||||
prettier@^2.1.2, prettier@^2.8.0:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.0.tgz#c7df58393c9ba77d6fba3921ae01faf994fb9dc9"
|
||||
integrity sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA==
|
||||
|
||||
Reference in New Issue
Block a user