Last active
June 3, 2025 13:50
-
-
Save thuykaka/24a195ea4ec770ae298e5326e3b1bac9 to your computer and use it in GitHub Desktop.
Tạo Bank QR (giống VietQR)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| https://vietqr.net/portal-service/download/documents/QR_Format_T&C_v1.0_VN_092021.pdf | |
| Nội dung của mã QR động sẽ có các phần ghép lại tạo thành 1 nội dung hoàn chỉnh (mỗi phần bao gồm mã ID, độ dài của giá trị và giá trị): | |
| 1. Payload Format Indicator | |
| Ví dụ: 000210 -> ID: 00, Độ dài: 02, Giá trị: 01. | |
| 2. Point of Initiation Method | |
| Ví dụ: 010212 -> ID: 01, Độ dài: 02, Giá trị: 12. | |
| Trong đó: | |
| - 11 = QR tĩnh – áp dụng khi mã QR cho phép thực hiện nhiều lần giao dịch | |
| - 12 = QR động – áp dụng khi mã QR chỉ cho phép thực hiện một lần giao dịch | |
| 3. Consumer Account Information | |
| Ví dụ: 38530010A000000727012300069704230109mynamebvh0208QRIBFTTA -> ID: 38, Độ dài: 53, Giá trị: 0010A000000727012300069704230109mynamebvh0208QRIBFTTA | |
| Giá trị gồm 3 thành phần: | |
| - Định danh toàn cầu (GUID): 0010A000000727 -> ID: 00, Độ dài: 10, Giá trị: A000000727 | |
| - Tổ chức ngân hàng thụ hưởng: 012300069704230109mynamebvh -> ID: 01, Độ dài: 23, Giá trị: 00069704230109mynamebvh. | |
| Giá trị này lại chia làm 2 phần: | |
| - Acquier ID/BNB ID: 0006970423 -> ID: 00, Độ dài: 06, Giá trị: 970423 (970423 chính là mã BIN TPBank). | |
| - Merchant ID/Consumer ID: 0109mynamebvh, ID: 01, Độ dài: 09, Giá trị: mynamebvh (Chính là số tài khoản). | |
| - Mã dịch vụ: 0208QRIBFTTA -> ID: 02, Độ dài: 08, Giá trị: QRIBFTTA (QRIBFTTA: dịch vụ chuyển nhanh NAPAS247 bằng mã QR đến Tài khoản) | |
| 4. Transaction Currency | |
| Ví dụ: 5303704 -> ID: 53, Độ dài: 03, Giá trị: 704 (704 mã tiền tệ VN) | |
| 5. Transaction Amount | |
| Ví dụ: 5406180000 -> ID: 54, Độ dài: 06, Giá trị: 180000 (số tiền chuyển khoản) | |
| 6. Country Code | |
| Ví dụ: 5802VN -> ID: 58, Độ dài: 02, Giá trị: VN (mã quốc gia) | |
| 7. Additional Data Field Template | |
| Ví dụ: 62080804test -> ID: 62, Độ dài: 08, Giá trị: 0804test | |
| Giá trị gồm: ID: 08, Độ dài 04, Giá trị: test (là nội dung chuyển khoản) | |
| 8. CRC | |
| 6304AB76 -> ID: 63, Độ dài: 04, Giá trị: AB76 | |
| Đây là một phương pháp kiểm tra tính toàn vẹn dữ liệu trong việc truyền tải thông tin. | |
| Ở đây họ sử dụng CRC 0xFFFF. | |
| Cách tính là lấy nội dung của QRCode như ví dụ trên sẽ là 00020101021238530010A000000727012300069704230109mynamebvh0208QRIBFTTA53037045405500005802VN62080804test6304 | |
| rồi cho qua hàm tính CRC sẽ ra kết quả: AB76. | |
| */ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import qrcode, { type QRCodeToBufferOptions, type QRCodeToDataURLOptions } from 'qrcode'; | |
| import { z } from 'zod'; | |
| import { Jimp } from 'jimp'; | |
| type PointOfInitMethod = 'static' | 'dynamic'; | |
| type QrCodeLogoOptions = { | |
| src: string; | |
| text?: string; | |
| backgroundColor?: string; | |
| size?: { | |
| w?: number; | |
| h?: number; | |
| }; | |
| position?: 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; | |
| }; | |
| // Zod schema for validation | |
| const VietQrSchema = z.object({ | |
| bankBin: z | |
| .string() | |
| .min(1, 'Bank BIN or name is required') | |
| .refine(val => /^[0-9]{6}$/.test(val) || Object.keys(BANK_BINS).includes(val.toLowerCase()), { | |
| message: 'Invalid bank BIN code or bank name', | |
| }), | |
| accNo: z | |
| .string() | |
| .min(1, 'Account number is required') | |
| .max(19, 'Account number must be at most 19 digits') | |
| .regex(/^[0-9]+$/, 'Account number must contain only digits'), | |
| amt: z | |
| .string() | |
| .min(1, 'Amount is required') | |
| .max(13, 'Amount must be at most 13 digits') | |
| .regex(/^[0-9]+$/, 'Amount must contain only digits') | |
| .refine(val => parseInt(val) > 0, { | |
| message: 'Amount must be a positive number', | |
| }), | |
| desc: z | |
| .string() | |
| .min(1, 'Description is required') | |
| .max(50, 'Description must be at most 50 characters') | |
| .regex(/^[a-zA-Z0-9\s]+$/, 'Description must contain only letters, numbers and spaces') | |
| .transform(val => val.trim()) | |
| .refine(val => val.length <= 50, { | |
| message: 'Description must be at most 50 characters after trimming', | |
| }), | |
| initMethod: z.enum(['static', 'dynamic']).optional(), | |
| }); | |
| type VietQrConfig = z.infer<typeof VietQrSchema>; | |
| // Bank BIN codes mapping | |
| const BANK_BINS: Record<string, string> = { | |
| // Ngân hàng TMCP Sài Gòn Công Thương | |
| saigonbank: '970400', | |
| sgb: '970400', | |
| 'ngan hang sai gon cong thuong': '970400', | |
| // Ngân hàng TMCP Sài Gòn Thương Tín | |
| sacombank: '970403', | |
| stb: '970403', | |
| 'ngan hang sai gon thuong tin': '970403', | |
| // Ngân hàng Nông nghiệp và Phát triển Nông thôn Việt Nam | |
| agribank: '970405', | |
| 'ngan hang nong nghiep va phat trien nong thon viet nam': '970405', | |
| // Ngân hàng TMCP Đông Á | |
| dongabank: '970406', | |
| 'ngan hang dong a': '970406', | |
| // Ngân hàng TMCP Kỹ Thương Việt Nam | |
| techcombank: '970407', | |
| tcb: '970407', | |
| 'ngan hang ky thuong viet nam': '970407', | |
| // Ngân hàng TNHH Một Thành Viên Dầu Khí Toàn Cầu | |
| gpbank: '970408', | |
| 'ngan hang dau khi toan cau': '970408', | |
| // Ngân hàng TMCP Bắc Á | |
| bab: '970409', | |
| 'ngan hang bac a': '970409', | |
| // Ngân hàng TNHH Một Thành Viên Standard Chartered | |
| stanchart: '970410', | |
| scvn: '970410', | |
| 'ngan hang standard chartered viet nam': '970410', | |
| // Ngân hàng TMCP Đại Chúng Việt Nam | |
| pvcombank: '970412', | |
| 'ngan hang dai chung viet nam': '970412', | |
| // Ngân hàng TNHH Một Thành Viên Đại Dương | |
| oceanbank: '970414', | |
| ocean: '970414', | |
| // Ngân hàng TMCP Công Thương Việt Nam | |
| vietinbank: '970415', | |
| 'ngan hang cong thuong viet nam': '970415', | |
| // Ngân hàng TMCP Á Châu | |
| acb: '970416', | |
| 'ngan hang a chau': '970416', | |
| // Ngân hàng Đầu tư và Phát triển Việt Nam | |
| bidv: '970418', | |
| 'ngan hang dau tu va phat trien viet nam': '970418', | |
| // Ngân hàng TMCP Quốc Dân | |
| ncb: '970419', | |
| 'ngan hang quoc dan': '970419', | |
| // Ngân hàng Liên doanh Việt Nga | |
| vrb: '970421', | |
| 'ngan hang lien doanh viet nga': '970421', | |
| // Ngân hàng TMCP Quân Đội | |
| mb: '970422', | |
| 'ngan hang quan doi': '970422', | |
| // Ngân hàng TMCP Tiên Phong | |
| tpbank: '970423', | |
| 'ngan hang tien phong': '970423', | |
| // Ngân hàng TNHH Một Thành Viên Shinhan Việt Nam | |
| shinhan: '970424', | |
| 'ngan hang shinhan viet nam': '970424', | |
| // Ngân hàng TMCP An Bình | |
| abbank: '970425', | |
| 'ngan hang an binh': '970425', | |
| // Ngân hàng TMCP Hàng Hải | |
| msb: '970426', | |
| 'ngan hang hang hai': '970426', | |
| // Ngân hàng TMCP Việt Á | |
| vietabank: '970427', | |
| 'ngan hang viet a': '970427', | |
| // Ngân hàng TMCP Nam Á | |
| nama: '970428', | |
| 'ngan hang nam a': '970428', | |
| // Ngân hàng TMCP Xăng dầu Petrolimex | |
| pgbank: '970430', | |
| 'ngan hang thinh vuong va phat trien': '970430', | |
| // Ngân hàng TMCP Xuất Nhập khẩu Việt Nam | |
| eximbank: '970431', | |
| 'ngan hang xuat nhap khau viet nam': '970431', | |
| // Ngân hàng TMCP Việt Nam Thịnh Vượng | |
| vpbank: '970432', | |
| 'ngan hang viet nam thinh vuong': '970432', | |
| // Ngân hàng TMCP Việt Nam Thương Tín | |
| vietbank: '970433', | |
| 'ngan hang viet nam thuong tin': '970433', | |
| // Ngân hàng TNHH Indovina | |
| indovina: '970434', | |
| 'ngan hang indovina': '970434', | |
| // Ngân hàng TMCP Ngoại thương Việt Nam | |
| vietcombank: '970436', | |
| vcb: '970436', | |
| 'ngan hang ngoai thuong viet nam': '970436', | |
| // Ngân hàng TMCP Phát triển TP.HCM | |
| hdbank: '970437', | |
| 'ngan hang phat trien tp hcm': '970437', | |
| // Ngân hàng TMCP Bảo Việt | |
| bvb: '970438', | |
| baovietbank: '970438', | |
| 'ngan hang bao viet': '970438', | |
| // Ngân hàng TNHH Một Thành Viên Public Việt Nam | |
| publicbank: '970439', | |
| pbvn: '970439', | |
| 'ngan hang public viet nam': '970439', | |
| // Ngân hàng TMCP Đông Nam Á | |
| seabank: '970440', | |
| 'ngan hang dong nam a': '970440', | |
| // Ngân hàng TMCP Quốc Tế Việt Nam | |
| vib: '970441', | |
| 'ngan hang quoc te viet nam': '970441', | |
| // Ngân hàng TNHH Một Thành Viên Hong Leong Việt Nam | |
| hongleong: '970442', | |
| hlbvn: '970442', | |
| 'ngan hang hong leong viet nam': '970442', | |
| // Ngân hàng TMCP Sài Gòn - Hà Nội | |
| shb: '970443', | |
| 'ngan hang sai gon ha noi': '970443', | |
| // Ngân hàng TNHH Một Thành Viên Xây Dựng Việt Nam | |
| cbbank: '970444', | |
| 'ngan hang xay dung viet nam': '970444', | |
| // Ngân hàng Hợp Tác Xã Việt Nam | |
| coopbank: '970446', | |
| // Ngân hàng TMCP Phương Đông | |
| ocb: '970448', | |
| // Ngân hàng TMCP Kiên Long | |
| kienlong: '970452', | |
| klb: '970452', | |
| // Ngân hàng TMCP Bản Việt | |
| bvbank: '970454', | |
| // Ngân hàng Công nghiệp Hàn Quốc - Chi nhánh Hà Nội | |
| ibk_hanoi: '970455', | |
| // Ngân hàng Industrial Bank of Korea - Chi nhánh Hồ Chí Minh | |
| ibk_hcm: '970456', | |
| // Ngân hàng TNHH Một Thành Viên Woori Bank Việt Nam | |
| wooribank: '970457', | |
| // Ngân hàng TNHH Một Thành Viên UOB Việt Nam | |
| uob: '970458', | |
| // Ngân hàng TNHH Một Thành Viên CIMB Việt Nam | |
| cimb: '970459', | |
| }; | |
| /** | |
| * Get bank BIN code from bank name | |
| * @param bankName - Bank name or code (case insensitive) | |
| * @returns Bank BIN code or undefined if not found | |
| */ | |
| function getBankBin(bankName: string): string | undefined { | |
| return BANK_BINS[bankName.toLowerCase()]; | |
| } | |
| /** | |
| * Check if a string is a valid bank BIN code | |
| * @param bin - String to check | |
| * @returns boolean indicating if the string is a valid BIN code | |
| */ | |
| function isValidBin(bin: string): boolean { | |
| return /^[0-9]{6}$/.test(bin); | |
| } | |
| /** | |
| * Get bank BIN code from either bank name or direct BIN code | |
| * @param input - Bank name or BIN code | |
| * @returns Bank BIN code or undefined if not found | |
| * @throws Error if input is invalid | |
| */ | |
| function resolveBankBin(input: string): string { | |
| // If input is a valid BIN code, use it directly | |
| if (isValidBin(input)) return input; | |
| // Try to get BIN code from bank name | |
| const bin = getBankBin(input); | |
| if (bin) return bin; | |
| throw new Error(`Invalid bank name or BIN code: ${input}, refer: https://www.sbv.gov.vn/webcenter/portal/vi/menu/trangchu/ttvnq/htmtcqht`); | |
| } | |
| class VietQr { | |
| private static readonly PFI = '000210'; // PAYLOAD_FORMAT_INDICATOR | |
| private static readonly GUID = '0010A000000727'; | |
| private static readonly SVC = '0208QRIBFTTA'; // SERVICE_CODE | |
| private static readonly CURR = '5303704'; // CURRENCY | |
| private static readonly CC = '5802VN'; // COUNTRY_CODE | |
| private readonly initMethod: string; | |
| private readonly accInfo: string; | |
| private readonly txAmt: string; | |
| private readonly addData: string; | |
| /** | |
| * Creates a new VietQR code generator instance | |
| * @param cfg Configuration object containing all required parameters | |
| * @param cfg.bankBin - Bank BIN code or bank name from https://www.sbv.gov.vn/webcenter/portal/vi/menu/trangchu/ttvnq/htmtcqht | |
| * @param cfg.accNo - Recipient's bank account number | |
| * @param cfg.amt - Transfer amount (positive number, max 13 digits) | |
| * @param cfg.desc - Transfer description (max 50 characters, no special characters) | |
| * @param cfg.initMethod - QR type: 'static' or 'dynamic' | |
| * @throws Error if validation fails | |
| */ | |
| constructor(cfg: VietQrConfig) { | |
| // Validate input using Zod schema | |
| const validatedConfig = VietQrSchema.parse(cfg); | |
| const bankBin = resolveBankBin(validatedConfig.bankBin); | |
| this.initMethod = this.buildInitMethod(validatedConfig.initMethod); | |
| this.accInfo = this.buildAccInfo(bankBin, validatedConfig.accNo); | |
| this.txAmt = this.buildTxAmt(validatedConfig.amt); | |
| this.addData = this.buildAddData(validatedConfig.desc); | |
| } | |
| private static getSize(txt: string): string { | |
| return txt.length.toString().padStart(2, '0'); | |
| } | |
| private buildInitMethod(method?: PointOfInitMethod): string { | |
| return method === 'static' ? '010211' : '010212'; | |
| } | |
| private buildAccInfo(bin: string, acc: string): string { | |
| const binSize = VietQr.getSize(bin); | |
| const binPart = `00${binSize}${bin}`; | |
| const accSize = VietQr.getSize(acc); | |
| const accPart = `01${accSize}${acc}`; | |
| const org = `01${VietQr.getSize(`${binPart}${accPart}`)}${binPart}${accPart}`; | |
| const info = `${VietQr.GUID}${org}${VietQr.SVC}`; | |
| return `38${VietQr.getSize(info)}${info}`; | |
| } | |
| private buildTxAmt(amt: string): string { | |
| return `54${VietQr.getSize(amt)}${amt}`; | |
| } | |
| private buildAddData(desc: string): string { | |
| const val = `08${VietQr.getSize(desc)}${desc}`; | |
| return `62${VietQr.getSize(val)}${val}`; | |
| } | |
| private static calcCrc(txt: string): string { | |
| let crc = 0xffff; | |
| for (let i = 0; i < txt.length; i++) { | |
| crc ^= txt.charCodeAt(i) << 8; | |
| for (let j = 0; j < 8; j++) { | |
| crc = (crc & 0x8000) !== 0 ? ((crc << 1) ^ 0x1021) & 0xffff : (crc << 1) & 0xffff; | |
| } | |
| } | |
| return crc.toString(16).toUpperCase().padStart(4, '0'); | |
| } | |
| /** | |
| * Generates the QR code string | |
| * @returns The complete QR code string | |
| */ | |
| public getQrContent(): string { | |
| const content = [VietQr.PFI, this.initMethod, this.accInfo, VietQr.CURR, this.txAmt, VietQr.CC, this.addData, '6304'].join(''); | |
| return `${content}${VietQr.calcCrc(content)}`; | |
| } | |
| public async getQrDataUrl(options: QRCodeToDataURLOptions): Promise<string> { | |
| return qrcode.toDataURL(this.getQrContent(), { width: 250, ...options }); | |
| } | |
| private detectUrlType(url: string): 'url' | 'base64' | undefined { | |
| if (/^data:image\/png;base64,/.test(url)) return 'base64'; | |
| if (z.string().url().safeParse(url).success) return 'url'; | |
| return undefined; | |
| } | |
| public async getQrWithLogo(qrCodeOptions: QRCodeToBufferOptions, logoOptions: QrCodeLogoOptions): Promise<string> { | |
| const qrImage = await Jimp.read(await qrcode.toBuffer(this.getQrContent(), { width: 250, ...qrCodeOptions })); | |
| const logoSrc = logoOptions.src; | |
| const logoType = this.detectUrlType(logoSrc); | |
| if (!logoType) throw new Error('Invalid logo source'); | |
| const logoImage = await Jimp.read(logoType === 'url' ? logoSrc : Buffer.from(logoSrc.split(',')[1]!, 'base64')); | |
| const logoRatio = logoImage.bitmap.height / logoImage.bitmap.width; | |
| const logoWidth = Math.min(logoOptions.size?.w ?? qrImage.bitmap.width * 0.25, qrImage.bitmap.width * 0.25); | |
| const logoHeight = logoOptions.size?.h ?? Math.round(logoWidth * logoRatio); | |
| logoImage.resize({ w: logoWidth, h: logoHeight }); | |
| // Center the logo in the QR code | |
| const x = qrImage.bitmap.width / 2 - logoWidth / 2; | |
| const y = qrImage.bitmap.height / 2 - logoHeight / 2; | |
| if (!logoOptions.backgroundColor) { | |
| qrImage.composite(logoImage, x, y); | |
| } else { | |
| const backgroundImage = new Jimp({ width: logoWidth, height: logoHeight, color: logoOptions.backgroundColor }); | |
| backgroundImage.composite(logoImage, 0, 0); | |
| qrImage.composite(backgroundImage, x, y); | |
| } | |
| return qrImage.getBase64('image/png'); | |
| } | |
| } | |
| export { VietQr, type VietQrConfig }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const vietQr = new VietQr({ bankBin: '970415', accNo: '0982447171', amt: '50000', desc: 'thuyck' }); | |
| const qrOptions = {}; | |
| const logoOptions = { | |
| src: `https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png`, | |
| backgroundColor: '#ffffff', | |
| }; | |
| const url = await vietQr.getQrWithLogo(qrOptions, logoOptions); | |
| console.log('url:', url); | |
| // data:image/png;base64,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JImabVokUbPNVJKYyjbLMhArRxJvunaN66+cccs9u9x1bJcTJ815e8XVzxWvv2aN555fOEt793H++z7I5s2/xtbtn2a+eZL9r38La2/5Hobnv4AztOf5zL7js+z+xVH86KdhbnThWynPuxFtXMgZ33K48KEf2svH7tjhT7+0y0XfXPiHV834tx8/zUzwzfuP8L1Xvp5LDl6E9u7jgv/4Xzj1ax9j687bmZ86xf7XvIE9//ifUC66mFgtA7GyDh8o3HB94Ybr1/jbaGMPe996A3vfegNPReuHGa58D0/loUfnfP7YnBtft4a0zhl33bfLGRceEpcdeh5vv/YHOUt79rLvhrex74a3EattIOJrPnzraW79kzn3PjjwhmsGvvI4fOCWTWYFrr9qIJ65BiK+5p+/dg9fObHJr965w6/eucMZazP4ides8fzDhXjmGoj4mvP2ivd93wZ33z/nC4/MOTQT11w+46ILCvHMNrBAkqjZZirbjCWJFtvUJDGWJMayTYskarZpkUSLbWqSGMs2X29tKHzHFYWXXSlWlW1qkhhLEi22qUmixTYtkqjZpkUSNdssQiEiuleIiO4VIqJ7hYjonmybJZLEIthmUSQxlm2mkMRUthlLEi22qUliVdmmRRI120wlibFsc64VIqJ7hYjoXiEiuleIiO4VIqJ7sm06JokW29Qk0WKbmiTGss2iSGIs29QkMZZtWiRRs02LJFpsM4UkprLNIkiixTbLUoiI7hUionuFiOheISK6N7BAkhjLNotgmxZJTGGbFklMIYkW27TYZhFsU5PEVLYZSxKrShIttllFhYjoXiEiuleIiO4VIqJ7hYjo3sCS2WYsSbTYZixJtNimJokWSdRsswi2WSbbtEiiZpsWSYwliRbbjGWbZZHEVLZpkUTNNotQiIjuFSKie4WI6F4hIrpXiIjuybZZIkm02GYKSbTYZpkksUy2qUmixTY1SSyTbVokUbNNiyRqtmmRRM02Y0ni6bBNTRIttlmWQkR0rxAR3StERPcKEdE92TYLIomabcaSRIttxpLEVLapSWIs2yyTJBbBNmNJ4umwTU0SY9lmmSQxlm1aJFGzzSIUIqJ7hYjoXiEiuleIiO4VIqJ7sm2WSBKLYJtnGklMZZuaJMayTYskxrJNTRIttmmRRM02LZKo2aZFEstkm1VUiIjuFSKie4WI6F4hIron22ZBJFGzzTJJosU2NUlMZZspJNFim6kksQi2GUsSLbapSaLFNjVJ9MA2i1CIiO4VIqJ7hYjoXiEiuleIiO4NLJBtzjXbnGuSaLHNWJJosU1NEotgm7Ek0WKbqSQxlm2mkESLbcaSxLlWiIjuFSKie4WI6F4hIron22ZBJLGqbDOFJMayzaJIomabFknUbNMiibFsM5YkWmwzliTGsk1NEi22qUlimWyzCIWI6F4hIrpXiIjuFSKie4WI6N7AktlmmSQxlSRqtlkFtqlJYixJtNhmCklMJYlFsM2i2KYmiRbbLEshIrpXiIjuFSKie4WI6N7AipDEFLaZShJTSaJmmxZJjGWbFknUbNMiiZptVoEkFkESyySJVVSIiO4VIqJ7hYjoXiEiuleIiO4NxJNs0yKJmiTGkkSLbcaSxFiSGEsSLbapSaLFNotgmxZJ1GzTIomabaaSRIttapI41woR0b1CRHSvEBHdK0RE9woR0b2BeJIkWmwzliRqtmmRxCLYpkUSNdssgm1aJNFim5okWmxTk8RYkmixTU0SLbZpkUTNNi2SqNlmEQoR0b1CRHSvEBHdK0RE9wZWhG3ONduMJYmxJNFim5okWmzTIomaJFpsU5NEi22mkMTTIYmxJFGzTYskarZpkUTNNi2SaLHNWLZZlkJEdK8QEd0rRET3ChHRvUJEdE+2zYJIYlXZpiaJc802LZJosc25JolFsM1YkpjKNlNJomabFknUbLMIhYjoXiEiuleIiO4VIqJ7sm0iomuFiOheISK6V4iI7hUionuFiOheISK6V4iI7hUionuFiOheISK6V4iI7hUionuFiOheISK6V4iI7hUionuFiOheISK6V4iI7hUionuFiOje/wVMuTioRy1+8AAAAABJRU5ErkJggg== |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment