12.22~
1. mongodb atlas 연결 (완료)
2. hardhat 서버 구축 (완료)
3. front verify -> hardhat compile -> parse source code (완료)
4. next-auth, wagmi, jwt 로그인 기능 구현 (완료))
5. sepolia testnet provider 설정 (완료)
6. schema 모델링 (완료)
7. monaco editor (완료)
8. abi interface 구현 (완료)
9. dapp (완료)
10. aws s3 sdk (완료)
11. aws cloudfront - s3 (완료)
12. image upload in mdapp (완료)
13. add contract library in verify (완료)
14. ragon contract deploy in testnet
15. md comment, mde 커스텀 툴바 구현 (완료)
16. 네트워크별 서브도메인 분리 (완료)
17. vercel 데모 배포 (완료 04.16)
18. 상표 출원 (완료 05.18)
19. 웹디자인, 반응형
20. 깃북
npm install mongodb mongoose
mongodb+srv://ragon01:<password>@mongodb-cluster.hxojwqv.mongodb.net/<dbname>?retryWrites=true&w=majority
mongoose
.connect(process.env.NEXT_PUBLIC_MONGODB_URI!)
에러가 발생해서 프로미스 타입으로 변경했다.
mongoose
.connect(process.env.NEXT_PUBLIC_MONGODB_URI!)
.then(() => console.log('MongoDB connected'))
.catch((err) => console.error('MongoDB connection error:', err));
mongoose.Promise = global.Promise;
⚠ ./node_modules/mongoose/node_modules/mongodb/lib/deps.js
Module not found: Can't resolve 'aws4' in 'C:\Users\USER\VsProject\ragon\node_modules\mongoose\node_modules\mongodb\lib'
aws4 설치
npm install aws4
models.Contract not found 런타임 에러 수정
export const Contract = mongoose.models?.Contract || mongoose.model('Contract', contractSchema);
model (collection)에 사용되는 modelName은 자동으로 소문자로 변환되고 복수형으로 변환된다.
ex) Contract -> contracts
connection은 단일 연결, createConnection은 다중 연결
connection은 mongoose가 단일 객체로 관리되지만 createConnection은 반환되는 Connection 객체를 저장해서 사용해야한다. 이 때, 중복 연결이 되지 않도록 유의해야한다.
socketTimeout은 네트워크 요청을 기다리는 시간, idleTimeout은 connection 연결 유지 시간
// id 또는 name 일치하는 도큐먼트
find({
$or: [
{ id: 0x123 },
{ name: user1 }
]
});
// A 혹은 B인 name 검색
"name":{
$in:["A","B"]
}
// where: 자바스크립트
find({ $where: "this.name === 'user1'"})
db.book.update({
"hits":110
}, {
$set:{
"hits":120
}
})
// hits:110인걸 검색해서 120으로 업데이트 하라
// update 첫번째인자는 검색 쿼리 key/field, 두번째 인자는 업데이트 쿼리 {$set: {key:field}}
comments, commentLikes 콜렉션의 경우 각 네트워크 데이터베이스에 저장되어 있으므로 집계 파이프라인을 사용하여 단일 쿼리로 데이터를 가져올 수 있음.
하지만, users를 공통 DB에서 관리하기 때문에 집계 파이프라인으로 가져올 수 없고 어플리케이션 레벨에서 조인하는 로직이 필요함.
const aggregation = [
{
$match: {
...queryFilters,
parentId: { $exists: false },
},
},
{ $skip: offset },
{ $limit: limit },
{
$lookup: {
from: 'comments',
localField: '_id',
foreignField: 'parentId',
as: 'childComments',
},
},
{
$addFields: {
childCount: { $size: '$childComments' },
},
},
{
$project: {
childComments: 0,
},
},
];
comment = await Comment.aggregate(aggregation);
문제: ragon common db를 mongoose connect로 기본 커넥션 객체를 사용, ragon network db를 mongoose createConnection으로 멀티 커넥션을 사용함. ragon network db에서 cascade delete처럼 연관된 콜렉션을 수정하기 위해 pre 미들웨어 훅을 사용했지만, network정보가 각 document에 존재하지 않기 때문에 어려움이 발생함.
해결: ragon common db를 createConnection, 기본 서브도메인에 대한 네트워크 연결을 mongoose.connect로 연결함. 서브도메인 변경시 하나의 connect를 연결해제 (disconnection, close) 후 재연결을 요청했지만 동시에 호출되는 모듈이 여러 처리를 동기적으로 처리하지 못했음. 재연결 방법을 useDb로 변경 (하나의 클러스트에 접근 후 다른 데이터베이스에 연결할 수 있음)
생략
스마트 컨트랙트 배포 트랜잭션인지 확인하는 방법
methodId: 0x60806040은 배포 메소드를 의미하지 않고 메모리 초기화를 의미한다. 그러므로, 트랜잭션 정합성을 위한 비교값으로 사용하기 부적절하다.
스마트 컨트랙트의 배포 트랜잭션 패턴은 다음과 같다.
서버 to 서버는 axios를 걷어내고 fetch 캐싱 사용
프론트 사이드 to 서버 사이드는 react query 적용
에디터는 react useCallback, useMemo로 에디터 컴포넌트 리렌더링 방지
인증이 필요한 모든 서버 액션은 프론트 to 서버로 fetch 사용
서버에서 nonce 생성 후 사용자에게 메타마스크 nonce 메세지 서명 요청
address, msg, signature를 가지고 auth verify 수행
기본적으로 signature를 이용해서 msg를 알아내는 것이 검증이 아니고, signature에서 v, r, s를 추출해서 ecrecover로 메세지를 보낸 EoA address를 검증한다.
export const authOptions = {...}
export const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
// authOptions callback type error
-> export default NextAuth({...})
jsonRPC는 infura apikey 없이 사용이 가능하다?
ethers 6.9.0 버전에서는 utils 없이 사용하도록 변경됨
ethers.utils.hashMessage();
-> ethers.hashMessage();
NextAuth 5.0.x 베타 버전 버그로 4.24.5로 다운그레이드
ragonErc20Test01: 0xE59C8Abb392F6C7769f1E0c4c77F03dBFDda3383
const RagonTest01Factory = await ethers.getContractFactory("RagonErc20Test01");
const RagonTest01 = await RagonTest01Factory.deploy(
"RAGON", "RGN"
);
ragonErc20Test02: 0x49AB8601953A2d4B6031abC8206c78B96Ac8c589
ragonErc20Test03: 0x982Fb94da8471D9F26B125Cac8f5dC360e771176
ragonErc20Test04: 0x9d148B3103385eaD6910f572e736Ff445412A16F
ragonErc20Test05: 0x151F22dC7fA3DdD3A71773eF229e0F44027460f3
ragonErc20Test10 (multi, non-verify): 0xc561801C937C58eB3706Fa2e3CAA2b39a6e6e104
ragonErc20Test11 (single-flatten, non-verify): 0x0D392E800EFD5dd58E52147e8b08a95FbFb0eFC4
ragonErc20Test12 (json-single-flat, non-verify): 0x96CEc0E710Fe334EC87755Bf7B6B132BAcbC310F
ragonErc20Test20 (contarct name: 220) (multi, for etherscan test): 0x9BC82026f4e6a5095eAD1fD7039Db6cffF5bB523
hardhat complie --no-metadata => etherscan verify fail?
metadata 없이 생성한 코드는 creation bytecode와 transaction input(컨트랙트 배포 트랜잭션) 과 일치하지 않는 문제가 있음.
따라서, metadata 없이 생성한 코드를 추가 검증하려면 deployed bytecode와 비교하여야 함.
1. deployed bytecode와 tx.input 비교하여 metadata(1), constructor arguments 추출
full verify=
2. hardhat으로 보내서 컴파일한 creation bytecode + constructor arguments(contract) == tx.input?
partial verify=
3. hardhat으로 보내서 컴파일한 creation bytecode - metadata(2) + contructor arguments(contract) == tx.input - metadata(1)?
--no-metadata verify=
4. hardhat으로 보내서 컴파일한 deployed bytecode - metadata(2) + contructor arguments(contract) == deployed bytecode - metadata(2) + constructor arguments(deployed)?
tag는 function signature를 사용,
interface에서는 function selector 사용
// reference: https://solidity-by-example.org/
struct Custom {
uint256 id;
string msg;
}
/*
"transfer(address,uint256)"
0xa9059cbb
*/
function getSelector(string calldata _func) external pure returns (bytes4) {
return bytes4(keccak256(bytes(_func)));
}
function transfer(Custom memory custom) public pure returns (string memory){
return "ragon";
}
/*
"transfer((uint256,string))"
0x996ce327
*/
solc 옵션에서는 abi에 function selector를 전달할 수 없음.
remix IDE에서 tuple 타입 데이터 전달 방법: [val, val2]
ragonTest14.sol address: 0x81a9fa15DfFB085602B8e1DBd3305823bE9D344b
ragonTest15.sol address: 0x468E0f55d199c87821290271Bbfc79611a379000
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
contract RagonTest16 {
struct Custom {
uint256 id;
string msg;
}
function getSelector(string calldata _func) external pure returns (bytes4) {
return bytes4(keccak256(bytes(_func)));
}
function transfer(Custom memory _custom, string memory _msg) public pure returns (uint256 id, string memory msg2){
return (_custom.id, _msg);
}
}
ragonTest16.sol address: 0xadc0EeB111113030ac3B6DFcD15903F3947e1F79
function areEqual(prevProps: any, nextProps: any) {
return (
prevProps.type === nextProps.type
);
export default React.memo(MdButton, areEqual);
areEqual에서 true를 반환해도, MdButton 컴포넌트가 리렌더링되는 문제가 발생했다.
문제는 redux에서 가져오는 state의 값이 변경되지 않았음에도, 새로운 객체를 반환하여 Mdapp 컴포넌트의 state의 변경을 인식하고 리렌더링이 발생한다.
const { mdappContracts } = useAppSelector((state) => state.mdappReducer.mdapp);
const selectMdappContracts = (state: RootState) => state.mdappReducer.mdapp;
export const getMdappContracts = createSelector(selectMdappContracts, (contract) => {
console.log(contract.mdappContracts);
return contract.mdappContracts;
});
const mdappContracts = useSelector(getMdappContracts);
Error: could not decode result data (value="0x", info={ "method": "balanceOf", "signature": "balanceOf(address)" }, code=BAD_DATA, version=6.9.0)
Error: unconfigured name (value="905067b3cC28EB7571526b44524A9D0C66CddBea", code=UNCONFIGURED_NAME, version=6.9.0)
abi is not iterable
contract runner does not support sending transactions
RagonDepositTest01 address: 0xa94210a5683Ba4F275393a4F4a1D1182183145cb
RagonDepositTest02 address: 0x1d73364C2A74350A9cEFB7D00F013e8E6Ff82694
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract RagonDepositTest02 {
address private owner;
mapping(address => uint) private deposits;
constructor() {
owner = msg.sender;
}
event Deposit(address indexed sender, uint amount);
event Withdraw(address indexed receiver, uint amount);
function deposit() external payable returns (address, uint){
require(msg.value >= 0.01 ether, "Minimum deposit is 0.01 ETH");
deposits[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
return (msg.sender, deposits[msg.sender]);
}
function withdraw(address payable to, uint amount) external {
require(msg.sender == owner, "Only the owner can withdraw");
require(address(this).balance >= amount, "Insufficient balance in contract");
to.transfer(amount);
emit Withdraw(to, amount);
}
function getContractBalance() external view returns (uint) {
return address(this).balance;
}
function getBalanceByAddress(address _address) external view returns (uint) {
return deposits[_address];
}
}
RagonWriteTest01 deployed to 0xb1378Cd3224D41DfA8c1972Cc117Fe56b3db117a
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
contract RagonWriteTest01 {
string private message;
function setMessage(string memory _message) external returns (string memory) {
message = _message;
return message;
}
function getMessage() external view returns (string memory) {
return message;
}
}
client (POST get url) - server (return createPresignedPost) - client (POST upload) - s3
{
"Version": "2012-10-17",
"Id": "PolicyForCloudFrontPrivateContent",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": "*",
"Action": "*",
"Resource": "..",
"Condition": {
"StringEquals": {
"AWS:SourceArn": ".."
},
"NotIpAddress": {
"aws:SourceIp": "-.-.-.-/32"
}
}
}
]
}
cloudFront 설정 후에는 Principal을 cloudFront only로 변경해야 함
등록, 수정, 삭제시 서버와 데이터를 동기화 시키지 않음.
이를 해결하기 위해 클라이언트와 서버의 데이터 불일치는 커서 기반 페이징으로 중복을 피하고 연속된 데이터를 가져올 수 있음.
따라서, 클라이언트에서 데이터 조회시 클라이언트 상태와 데이터의 중복검사 로직이 필요없음.
Comment.find({ createdAt: { $lt: lastCursor } })
const result = await checkNetworkSync();
export function useNetworkSync() {
const [status, setStatus] = useState(false);
const { chain } = useNetwork();
...
const checkNetworkSync = async () => {
...
Wagmi 네트워크 변경 훅을 커스텀 훅으로 감싸서 기능을 호출했는데, 한번에 동기식으로 호출되지 않는 문제가 발생함. 확인 결과, wagmi에서 await connectAsync 훅을 connect 동기식으로 사용해서 로그인을 해도 내부적으로 상태변경을 비동기로 동작하여 인식을 못하는 것으로 보임.
wagmi react hook 대신, core 함수 호출하는 로직으로 변경해서 해결
useSwitchNetwork - React Hooks for Ethereum - Wagmi
switchNetwork – @wagmi/core
Partial로 타입을 선언한 경우, 해당 타입 내부의 객체정보는 Partial로 옵셔널 프로퍼티가 적용되지 않는 문제가 있음.
따라서, 다음과 같은 DeepPartial 타입을 적용했다.
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
또 다른 문제는 다음과 같이 발생했다. 내부 객체를 명시적으로 포함하여 선언했음에도 객체 정보가 undefined 타입일 수 있다는 에러가 발생했다.
const newMdapp: DeepPartial<MdappExtendsType> = {
...
options: {
isPublic: isPublic,
isTemporarySave: false,
},
};
//문제는 위에서 options를 명시하여 선언했음에도 타입스크립트는 타입 에러를 발생
newMdapp.options.isTemporarySave = isTemporarySave;
'newMdapp.options' is possibly 'undefined'.
etherscan -> infura -> alchemy 로 마이그레이션을 진행했다.
alchemy sdk 설치 후 json rpc 호출 테스트에서 클라이언트 사이드 컴포넌트에서 호출한 경우 정상 응답을 받지만, 서버 사이드에서 호출한 경우 다음과 같은 에러가 발생했다.
Error: missing response (requestBody="{\"method\":\"eth_getTransactionByHash\",\"params\
at Logger.makeError (webpack-internal:///(rsc)/./node_modules/@ethersproject/logger/lib.esm/index.js:240:23)
at Logger.throwError (webpack-internal:///(rsc)/./node_modules/@ethersproject/logger/lib.esm/index.js:249:20)
at eval (webpack-internal:///(rsc)/./node_modules/@ethersproject/web/lib.esm/index.js:264:32)
at Generator.throw (<anonymous>)
at rejected (webpack-internal:///(rsc)/./node_modules/@ethersproject/web/lib.esm/index.js:31:40)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
reason: 'missing response',
code: 'SERVER_ERROR',
스택오버플로우에 같은 오류 현상을 발견했지만 아직 해결되지 않았다.
따라서, 클라이언트 ragon-read에서 사용하는 것 처럼 서버에서 ethers 라이브러리로 alchemy provider 객체를 통해 json rpc 호출하는 방식을 사용하여 다음과 같이 해결했다.
const alchemy = getAlchemyInstance(networkInfo.name);
const alchemyProvider = new ethers.AlchemyProvider('sepolia', process.env.NEXT_PUBLIC_ALCHEMY_PROVIDER_ACCESS_KEY);
const transaction = await alchemyProvider.getTransaction('0xd86064035c3baf7064930f819a0ad68c25ed6ea85e5a16b1b6ac83913a25fe55');
alchemy sdk 문제가 해결되면 다시 수정해서 서버에서는 alchemy sdk, 클라이언트에서는 ethers 사용하는 것으로 롤백할 예정
하나의 프로젝트 내에서 메인넷과 테스트넷 네트워크를 나누기 위해 파라미터 방식을 사용해서 개발했다.
공통 헤더에서 네트워크의 변경을 감지하고, redux 상태관리를 통해 하위 컴포넌트에서 항상 네트워크 동기화 로딩이 끝난 후 동작하도록 구현했다.
파라미터 방식은 SEO 최적화에 불리하고 상대적으로 URL 길이가 길어지게 됐다.
따라서 서브도메인 방식으로 변경했다.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const host = request.headers.get('host');
const subdomain = host ? host.split('.')[0] : '';
if (subdomain === 'sepolia') {
return NextResponse.rewrite(new URL('/sepolia', request.url))
}
}
middleware.ts 파일은 /src 폴더 내부에 생성
기존 /src/app/[..old] 파일은 /src/app/[network]/[..new] 방식으로 이동했다.
클라이언트 컴포넌트만 rewrite로 변경했으므로 /app/api 기존 서버코드는 동일하게 하고 다음과 같이 네트워크 파싱 방식만 변경했다.
const networkInfo = parseHeaderHostToNetwork(req.headers.get('host'));
if(!networkInfo){
return NextResponse.json(
{ status: 500 },
);
}